From d79fee0db301f826c100572d95664787983ccc46 Mon Sep 17 00:00:00 2001 From: vanDeventer Date: Mon, 19 May 2025 08:06:02 +0200 Subject: [PATCH 01/81] working on the pump --- README.md | 2 + revolutionary/README.md | 81 ++++++ revolutionary/mbaigo System revolutionary.txt | 77 ++++++ revolutionary/revolutionary.go | 189 ++++++++++++++ revolutionary/thing.go | 231 ++++++++++++++++++ telegrapher/telegrapher.go | 2 +- telegrapher/thing.go | 38 +-- 7 files changed, 601 insertions(+), 19 deletions(-) create mode 100644 revolutionary/README.md create mode 100644 revolutionary/mbaigo System revolutionary.txt create mode 100644 revolutionary/revolutionary.go create mode 100644 revolutionary/thing.go diff --git a/README.md b/README.md index 0432dfe..cc6e617 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ Many of the testing is done with the Raspberry Pi (3, 4, &5) with [GPIO](https:/ - 20150 ds18b20 (1-wire sensor) - 20151 Parallax (PWM) - 20152 Thermostat +- 20153 Revolutionary (Rev Pi PLC) +- 20154 Levler (Level control) - 20160 Picam - 20161 USB microphone - 20170 UA client (OPC UA) diff --git a/revolutionary/README.md b/revolutionary/README.md new file mode 100644 index 0000000..677bd48 --- /dev/null +++ b/revolutionary/README.md @@ -0,0 +1,81 @@ +# mbaigo System: revolutionary + +## Purpose +The revolutionary system runs on [Revolution Pi Connect 4](https://revolutionpi.com/documentation/revpi-connect-4/) PLC with an analogue input/output module within a pump station demonstrator. +It measures the level of two upper tanks (-10 to +10 Volts) and outputs a control signal to the pump (0 to +10 Volts). + + +## Sequence of events +This system offers three services: the two level sensors and the pump speed signal. +For the services to be discoverable, they must be in the registry of currently available services. +The consuming system asks the Orchestrator for the desired service by describing it. +The orchestrator inquires with the Service Registrar if the service is available and if so provides the service URL. +The consuming system will request the service directly from the Revolutionary system via this URL until it stops working. + +```mermaid +sequenceDiagram + +participant ServiceRegistrar as ServiceRegistrar +participant Orchestrator as Orchestrator +participant Revolutionary as Revolutionary +participant ArrowheadSystem as Any Arrowhead System + +loop Before registration expiration + activate Revolutionary + Revolutionary->>+ServiceRegistrar: Register each service + ServiceRegistrar-->>-Revolutionary: New expiration time + deactivate Revolutionary +end + + +loop Every x period + activate ArrowheadSystem + + alt Service location is unknown + ArrowheadSystem->>+Orchestrator: Discover service provider + activate Orchestrator + Orchestrator->>+ServiceRegistrar: Query for service + ServiceRegistrar-->>-Orchestrator: Return service location + Orchestrator-->>ArrowheadSystem: Forward service location + deactivate Orchestrator + else Service location is known + Note over ArrowheadSystem: Use cached location + end + + loop For each wanted service + ArrowheadSystem->>Revolutionary: Get statistics from service + activate Revolutionary + Revolutionary-->>ArrowheadSystem: Return latest data + deactivate Revolutionary + ArrowheadSystem->>ArrowheadSystem: Cache sampled data + end + + deactivate ArrowheadSystem +end +``` + +## Configuration +The default configuration using the [Rev Pi AIO](https://revolutionpi.com/documentation/revpi-aio/) module is the upper tank level sensor (v+ @ pin 28, GND @ pin 24). Its name is **InputValue_1** + +At deploment, the two other unit assets need to be provided (into the array of unit assets in the *systemconfig.json* file): +- the middle tank level sensor (v+ @ pin 17, GND @ pin 23) has the name **InputValue_1**. +- The pump signal (v+ @ pin 1 with GND @ pin 5) has the name **OutputValue_1**. + +## Compiling +To compile the code, one needs to get the mbaigo module +```go get github.com/sdoque/mbaigo``` +and initialize the *go.mod* file with ``` go mod init github.com/sdoque/systems/Revolutionary``` before running *go mod tidy*. + +To run the code, one just needs to type in ```go run .``` within a terminal or at a command prompt. (But that works only on a Revolution Pi, since it is accessing that hardware.) + +It is **important** to start the program from within its own directory (and each system should have their own directory) because program looks for its configuration file there. If it does not find it there, it will generate one and shutdown to allow the configuration file to be updated. + +The configuration and operation of the system can be verified using the system's web server using a standard web browser, whose address is provided by the system at startup. + +## Cross compiling/building +The following commands enable one to build for a different platform: + +- Raspberry Pi 64: ```GOOS=linux GOARCH=arm64 go build -o Revolutionary_rpi64 ``` + +If one wants to secure copy it to a Revolution pi, +`scp Revolutionary_rpi64 username@ipAddress:mbaigo/Revolutionary/` where user is the *username* @ the *IP address* of the Raspberry Pi with a relative (to the user's home directory) destination (the *mbaigo/Revolutionary/* directory in this case). diff --git a/revolutionary/mbaigo System revolutionary.txt b/revolutionary/mbaigo System revolutionary.txt new file mode 100644 index 0000000..4253e67 --- /dev/null +++ b/revolutionary/mbaigo System revolutionary.txt @@ -0,0 +1,77 @@ +# mbaigo System: revolutionary + +## Purpose +The revolutionary system runs on [Revolution Pi Connect 4](https://revolutionpi.com/documentation/revpi-connect-4/) PLC with an analogue input/output module within a pump station demonstrator. +It measures the level of two upper tanks (-10 to +10 Volts) and outputs a control signal to the pump (0 to +10 Volts). + + +## Sequence of events +This system offers three services: the two level sensors and the pump speed signal. +For the services to be discoverable, they must be in the registry of currently available services. +The consuming system asks the Orchestrator for the desired service by describing it. +The orchestrator inquires with the Service Registrar if the service is available and if so provides the service URL. +The consuming system will request the service directly from the Revolutionary system via this URL until it stops working. + +```mermaid +sequenceDiagram + +participant ServiceRegistrar as ServiceRegistrar +participant Orchestrator as Orchestrator +participant Revolutionary as Revolutionary +participant ArrowheadSystem as Any Arrowhead System + +loop Before registration expiration + activate Revolutionary + Revolutionary->>+ServiceRegistrar: Register each service + ServiceRegistrar-->>-Revolutionary: New expiration time + deactivate Revolutionary +end + + +loop Every x period + activate ArrowheadSystem + + alt Service location is unknown + ArrowheadSystem->>+Orchestrator: Discover service provider + activate Orchestrator + Orchestrator->>+ServiceRegistrar: Query for service + ServiceRegistrar-->>-Orchestrator: Return service location + Orchestrator-->>ArrowheadSystem: Forward service location + deactivate Orchestrator + else Service location is known + Note over ArrowheadSystem: Use cached location + end + + loop For each wanted service + ArrowheadSystem->>Revolutionary: Get statistics from service + activate Revolutionary + Revolutionary-->>ArrowheadSystem: Return latest data + deactivate Revolutionary + ArrowheadSystem->>ArrowheadSystem: Cache sampled data + end + + deactivate ArrowheadSystem +end +``` + +## Configuration +The default configuration using the [Rev Pi AIO](https://revolutionpi.com/documentation/revpi-aio/) module is the upper tank level sensor (v+ @ pin 28, GND @ pin 24), middle tank level sensor (v+ @ pin 17, GND @ pin 23). The (output) pump signal is on v+ @ pin 1 with GND @ pin 5. + +## Compiling +To compile the code, one needs to get the mbaigo module +```go get github.com/sdoque/mbaigo``` +and initialize the *go.mod* file with ``` go mod init github.com/sdoque/systems/Revolutionary``` before running *go mod tidy*. + +To run the code, one just needs to type in ```go run .``` within a terminal or at a command prompt. (But that works only on a Revolution Pi, since it is accessing that hardware.) + +It is **important** to start the program from within its own directory (and each system should have their own directory) because program looks for its configuration file there. If it does not find it there, it will generate one and shutdown to allow the configuration file to be updated. + +The configuration and operation of the system can be verified using the system's web server using a standard web browser, whose address is provided by the system at startup. + +## Cross compiling/building +The following commands enable one to build for a different platform: + +- Raspberry Pi 64: ```GOOS=linux GOARCH=arm64 go build -o Revolutionary_rpi64 ``` + +If one wants to secure copy it to a Revolution pi, +`scp Revolutionary_rpi64 username@ipAddress:mbaigo/Revolutionary/` where user is the *username* @ the *IP address* of the Raspberry Pi with a relative (to the user's home directory) destination (the *mbaigo/Revolutionary/* directory in this case). diff --git a/revolutionary/revolutionary.go b/revolutionary/revolutionary.go new file mode 100644 index 0000000..55322f9 --- /dev/null +++ b/revolutionary/revolutionary.go @@ -0,0 +1,189 @@ +/******************************************************************************* + * Copyright (c) 2024 Synecdoque + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, subject to the following conditions: + * + * The software is licensed under the MIT License. See the LICENSE file in this repository for details. + * + * Contributors: + * Jan A. van Deventer, Luleå - initial implementation + * Thomas Hedeler, Hamburg - initial implementation + ***************************************************************************SDG*/ + +package main + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log" + "mime" + "net/http" + "time" + + "github.com/sdoque/mbaigo/components" + "github.com/sdoque/mbaigo/forms" + "github.com/sdoque/mbaigo/usecases" +) + +func main() { + // prepare for graceful shutdown + ctx, cancel := context.WithCancel(context.Background()) // create a context that can be cancelled + defer cancel() // make sure all paths cancel the context to avoid context leak + + // instantiate the System + sys := components.NewSystem("revolutionary", ctx) + + // instantiate the husk + sys.Husk = &components.Husk{ + Description: "interacts with the RevPi Connect 4 PLC", + Details: map[string][]string{"Developer": {"Synecdoque"}}, + ProtoPort: map[string]int{"https": 0, "http": 20153, "coap": 0}, + InfoLink: "https://github.com/sdoque/systems/tree/main/revolutionary", + } + + // instantiate a template unit asset + assetTemplate := initTemplate() + assetName := assetTemplate.GetName() + sys.UAssets[assetName] = &assetTemplate + + // Configure the system + rawResources, servsTemp, err := usecases.Configure(&sys) + if err != nil { + log.Fatalf("configuration error: %v\n", err) + } + sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) + for _, raw := range rawResources { + var uac UnitAsset + if err := json.Unmarshal(raw, &uac); err != nil { + log.Fatalf("resource configuration error: %+v\n", err) + } + ua, cleanup := newResource(uac, &sys, servsTemp) + defer cleanup() + sys.UAssets[ua.GetName()] = &ua + } + + // Generate PKI keys and CSR to obtain a authentication certificate from the CA + usecases.RequestCertificate(&sys) + + // Register the (system) and its services + usecases.RegisterServices(&sys) + + // start the requests handlers and servers + go usecases.SetoutServers(&sys) + + // wait for shutdown signal, and gracefully close properly goroutines with context + <-sys.Sigs // wait for a SIGINT (Ctrl+C) signal + log.Println("\nshuting down system", sys.Name) + cancel() // cancel the context, signaling the goroutines to stop + time.Sleep(2 * time.Second) // allow the go routines to be executed, which might take more time than the main routine to end +} + +// Serving handles the resources services. NOTE: it expects those names from the request URL path +func (ua *UnitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath string) { + switch servicePath { + case "access": + ua.access(w, r) + default: + http.Error(w, "Invalid service request [Do not modify the services subpath in the configuration file]", http.StatusBadRequest) + } +} + +// access gets the unit asset's AIO channel datum and sends it in a signal form +func (ua *UnitAsset) access(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + getMeasuremet := STray{ + Action: "read", + Sample: make(chan forms.SignalA_v1a), + Error: make(chan error), + } + ua.sampleChan <- getMeasuremet + select { + case err := <-getMeasuremet.Error: + fmt.Printf("Logic error in getting measurement, %s\n", err) + w.WriteHeader(http.StatusInternalServerError) // Use 500 for an internal error + return + case signalForm := <-getMeasuremet.Sample: + usecases.HTTPProcessGetRequest(w, r, &signalForm) + return + case <-time.After(5 * time.Second): // Optional timeout + http.Error(w, "Request timed out", http.StatusGatewayTimeout) + log.Println("Failure to process signal reading request") + return + } + case "POST", "PUT": + contentType := r.Header.Get("Content-Type") + mediaType, _, err := mime.ParseMediaType(contentType) + if err != nil { + fmt.Println("Error parsing media type:", err) + return + } + + defer r.Body.Close() + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + log.Printf("error reading service discovery request body: %v", err) + return + } + serviceReq, err := usecases.Unpack(bodyBytes, mediaType) + if err != nil { + log.Printf("error extracting the service discovery request %v\n", err) + return + } + + speed, ok := serviceReq.(*forms.SignalA_v1a) + if !ok { + log.Println("problem unpacking the temperature signal form") + return + } + // Create a struct to send on a channel to handle the request + readRecord := STray{ + Action: "write", + Sample: speed, + Result: make(chan []forms.ServiceRecord_v1), + Error: make(chan error), + } + + // Send request to add a record to the unit asset + ua.requests <- readRecord + + // Use a select statement to wait for responses on either the Result or Error channel + select { + case err := <-readRecord.Error: + if err != nil { + log.Printf("Error retrieving service records: %v", err) + http.Error(w, "Error retrieving service records", http.StatusInternalServerError) + return + } + case servvicesList := <-readRecord.Result: + fmt.Println(servvicesList) + var slForm forms.ServiceRecordList_v1 + slForm.NewForm() + slForm.List = servvicesList + updatedRecordBytes, err := usecases.Pack(&slForm, mediaType) + if err != nil { + log.Printf("error confirming new service: %s", err) + http.Error(w, "Error registering service", http.StatusInternalServerError) + } + w.Header().Set("Content-Type", mediaType) + w.WriteHeader(http.StatusOK) + _, err = w.Write([]byte(updatedRecordBytes)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + case <-time.After(5 * time.Second): // Optional timeout + http.Error(w, "Request timed out", http.StatusGatewayTimeout) + log.Println("Failure to process service discovery request") + return + } + default: + http.Error(w, "Method is not supported.", http.StatusNotFound) + } +} diff --git a/revolutionary/thing.go b/revolutionary/thing.go new file mode 100644 index 0000000..021fb65 --- /dev/null +++ b/revolutionary/thing.go @@ -0,0 +1,231 @@ +/******************************************************************************* + * Copyright (c) 2024 Synecdoque + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, subject to the following conditions: + * + * The software is licensed under the MIT License. See the LICENSE file in this repository for details. + * + * Contributors: + * Jan A. van Deventer, Luleå - initial implementation + * Thomas Hedeler, Hamburg - initial implementation + ***************************************************************************SDG*/ + +package main + +import ( + "context" + "fmt" + "log" + "os" + "os/exec" + "strconv" + "strings" + "time" + + "github.com/sdoque/mbaigo/components" + "github.com/sdoque/mbaigo/forms" +) + +// Define the types of requests the measurement manager can handle +type STray struct { + Action string + Sample chan forms.SignalA_v1a + Error chan error +} + +//-------------------------------------Define the unit asset + +// UnitAsset type models the unit asset (interface) of the system. +type UnitAsset struct { + Name string `json:"name"` + Owner *components.System `json:"-"` + Details map[string][]string `json:"details"` + ServicesMap components.Services `json:"-"` + CervicesMap components.Cervices `json:"-"` + // + value float64 `json:"-"` + tStamp time.Time `json:"-"` + sampleChan chan STray `json:"-"` // Add a channel for signal reading +} + +// GetName returns the name of the Resource. +func (ua *UnitAsset) GetName() string { + return ua.Name +} + +// GetServices returns the services of the Resource. +func (ua *UnitAsset) GetServices() components.Services { + return ua.ServicesMap +} + +// GetCervices returns the list of consumed services by the Resource. +func (ua *UnitAsset) GetCervices() components.Cervices { + return ua.CervicesMap +} + +// GetDetails returns the details of the Resource. +func (ua *UnitAsset) GetDetails() map[string][]string { + return ua.Details +} + +// ensure UnitAsset implements components.UnitAsset (this check is done at during the compilation) +var _ components.UnitAsset = (*UnitAsset)(nil) + +//-------------------------------------Instantiate a unit asset template + +// initTemplate initializes a UnitAsset with default values. +func initTemplate() components.UnitAsset { + // Define the services that expose the capabilities of the unit asset(s) + access := components.Service{ + Definition: "access", + SubPath: "access", + Details: map[string][]string{"Forms": {"SignalA_v1a"}}, + RegPeriod: 30, + Description: "reads the input (GET) or chandes the outup (POST) of the channel", + } + + // var uat components.UnitAsset // this is an interface, which we then initialize + uat := &UnitAsset{ + Name: "InputValue_1", + Details: map[string][]string{"Unit": {"Volts"}, "Location": {"UpperTank"}}, + ServicesMap: components.Services{ + access.SubPath: &access, // add the service to the map + }, + } + return uat +} + +//-------------------------------------Instantiate the unit assets based on configuration + +// newResource creates the Resource resource with its pointers and channels based on the configuration +func newResource(uac UnitAsset, sys *components.System, servs []components.Service) (components.UnitAsset, func()) { + ua := &UnitAsset{ // this a struct that implements the UnitAsset interface + Name: uac.Name, + Owner: sys, + Details: uac.Details, + ServicesMap: components.CloneServices(servs), + sampleChan: make(chan STray), // Initialize the channel + } + + // start the unit asset(s) + go ua.sampleSignal(sys.Ctx) + + return ua, func() { + log.Printf("disconnecting from %s\n", ua.Name) + } +} + +//-------------------------------------Unit asset's functionalities + +// sampleSignal obtains the temperature from respective Rev Pi AIO resource at regular intervals +func (ua *UnitAsset) sampleSignal(ctx context.Context) { + defer close(ua.sampleChan) // Ensure the channel is closed when the goroutine exits + + // Create a ticker that triggers every second + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() // Clean up the ticker when done + + sigChan := make(chan float64) // Channel for latest signal readings + tStampChan := make(chan time.Time) + + // Start a separate goroutine for signal reading + go func() { + for { + select { + case <-ctx.Done(): // Stop when the context is canceled + os.Exit(0) + return + + case <-ticker.C: // sample the signal at regular intervals + v, err := readInputVoltage(ua.Name) + if err != nil { + fmt.Println("Read error:", err) + } else { + fmt.Printf("%s = %.2f V\n", ua.Name, v) + } + + // Send the sampeled signal and timestamp back to the main loop + select { + case sigChan <- v: + tStampChan <- time.Now() + case <-ctx.Done(): // Stop the goroutine if context is canceled + return + } + } + } + }() + + for { + select { + case sigValue := <-sigChan: // Update signal value and timestamp + ua.value = sigValue + ua.tStamp = <-tStampChan + + case order := <-ua.sampleChan: + switch order.Action { + case "read": + // Send the latest signal value and timestamp to the channel + var f forms.SignalA_v1a + f.NewForm() + f.Value = ua.value + f.Unit = "Volts" + f.Timestamp = ua.tStamp + order.Sample <- f + case "write": + // Receive the form from the channel + f := <-order.Sample + ua.value = f.Value + rawValue := voltageToRaw(ua.value) + err := writeOutput(ua.Name, rawValue) + if err != nil { + order.Error <- fmt.Errorf("write error: %w", err) + } + default: + order.Error <- fmt.Errorf("invalid action: %s", order.Action) + } + } + } +} + +// readInput reads the input value from the piTest command line tool. +func readInputVoltage(varName string) (float64, error) { + fmt.Println("Reading input:", varName) + cmd := exec.Command("/usr/bin/piTest", "-1", "-q", "-r", varName) + cmd.Stderr = os.Stderr + output, err := cmd.Output() + if err != nil { + return 0, fmt.Errorf("reading the Rev Pi failed: %w", err) + } + + valueStr := strings.TrimSpace(string(output)) + raw, err := strconv.Atoi(valueStr) + if err != nil { + return 0, fmt.Errorf("invalid raw value: %w", err) + } + + voltage := (float64(raw)/65535.0)*20.0 - 10.0 + return voltage, nil +} + +// writeOutput writes the output value to the piTest command line tool. +func writeOutput(varName string, value int) error { + fmt.Printf("Writing %d to %s\n", value, varName) + cmd := exec.Command("/usr/bin/piTest", "-w", fmt.Sprintf("%s,%d", varName, value)) + cmd.Stderr = os.Stderr + return cmd.Run() +} + +// voltageToRaw converts a voltage value to a raw value for the piTest command line tool. +func voltageToRaw(voltage float64) int { + if voltage < 0 { + voltage = 0 + } + if voltage > 10 { + voltage = 10 + } + return int((voltage / 10.0) * 65535.0) +} diff --git a/telegrapher/telegrapher.go b/telegrapher/telegrapher.go index 1f6a825..89a5ba6 100644 --- a/telegrapher/telegrapher.go +++ b/telegrapher/telegrapher.go @@ -98,7 +98,7 @@ func (ua *UnitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath func (ua *UnitAsset) access(w http.ResponseWriter, r *http.Request, servicePath string) { switch r.Method { case "GET": - msg := messageList[ua.metatopic+"/"+servicePath] + msg := messageList[ua.topic] if msg != nil { w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/json") diff --git a/telegrapher/thing.go b/telegrapher/thing.go index 6940ead..4b8e9ba 100644 --- a/telegrapher/thing.go +++ b/telegrapher/thing.go @@ -127,7 +127,7 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi // Define the message handler callback messageHandler := func(client mqtt.Client, msg mqtt.Message) { - fmt.Printf("Received message: %s from topic: %s\n", msg.Payload(), msg.Topic()) + fmt.Printf("Received A message: %s from topic: %s\n", msg.Payload(), msg.Topic()) // Ensure the map is initialized (just in case) if messageList == nil { @@ -137,6 +137,8 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi messageList[msg.Topic()] = msg.Payload() // Assign message to topic in the map } + log.Printf("The message list is:\n%+v\n", messageList) + for _, topicItem := range uac.Topics { // Consider the last term of a topic to be a service, and the preceding part is the asset lastSlashIndex := strings.LastIndex(topicItem, "/") @@ -144,9 +146,9 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi fmt.Printf("topic %s has no forward slash and is ignored\n", topicItem) continue } - a := topicItem[:lastSlashIndex] // The asset part - s := topicItem[lastSlashIndex+1:] // The service part + a := topicItem[:lastSlashIndex] // The asset part aName := strings.ReplaceAll(a, "/", "_") + s := topicItem[lastSlashIndex+1:] // The service part // Redefine the service access := components.Service{ @@ -203,18 +205,18 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi //-------------------------------------Unit asset's resource functions -// subscribeToTopic subscribes to the given MQTT topic -func (ua *UnitAsset) subscribeToTopic() { - // Define the message handler callback - messageHandler := func(client mqtt.Client, msg mqtt.Message) { - fmt.Printf("Received message: %s from topic: %s\n", msg.Payload(), msg.Topic()) - // ua.message = msg.Payload() - } - - // Subscribe to the topic - theTopic := ua.metatopic + "/" + ua.serviceDef - if token := ua.client.Subscribe(theTopic, 0, messageHandler); token.Wait() && token.Error() != nil { - log.Fatalf("Error subscribing to topic: %v", token.Error()) - } - fmt.Printf("Subscribed to topic: %s\n", theTopic) -} +// // subscribeToTopic subscribes to the given MQTT topic +// func (ua *UnitAsset) subscribeToTopic() { +// // Define the message handler callback +// messageHandler := func(client mqtt.Client, msg mqtt.Message) { +// ua.message = string(msg.Payload()) // Convert []byte to string +// fmt.Printf("Received message: %s from topic: %s\n", ua.message, msg.Topic()) +// } + +// // Subscribe to the topic +// theTopic := ua.metatopic + "/" + ua.serviceDef +// if token := ua.client.Subscribe(theTopic, 0, messageHandler); token.Wait() && token.Error() != nil { +// log.Fatalf("Error subscribing to topic: %v", token.Error()) +// } +// fmt.Printf("Subscribed to topic: %s\n", theTopic) +// } From af08bb6326d73f80b5e77d9076f18ecf8f25d465 Mon Sep 17 00:00:00 2001 From: vanDeventer Date: Mon, 19 May 2025 08:09:40 +0200 Subject: [PATCH 02/81] forgetten save --- revolutionary/revolutionary.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/revolutionary/revolutionary.go b/revolutionary/revolutionary.go index 55322f9..a754fe8 100644 --- a/revolutionary/revolutionary.go +++ b/revolutionary/revolutionary.go @@ -118,6 +118,7 @@ func (ua *UnitAsset) access(w http.ResponseWriter, r *http.Request) { return } case "POST", "PUT": + contentType := r.Header.Get("Content-Type") mediaType, _, err := mime.ParseMediaType(contentType) if err != nil { @@ -142,16 +143,14 @@ func (ua *UnitAsset) access(w http.ResponseWriter, r *http.Request) { log.Println("problem unpacking the temperature signal form") return } - // Create a struct to send on a channel to handle the request - readRecord := STray{ + setSignal := STray{ Action: "write", - Sample: speed, - Result: make(chan []forms.ServiceRecord_v1), + Sample: make(chan forms.SignalA_v1a), Error: make(chan error), } // Send request to add a record to the unit asset - ua.requests <- readRecord + ua.sampleChan <- setSignal // Use a select statement to wait for responses on either the Result or Error channel select { From 41dca5feba7324916b38b8cf207a9263dbe4e2bd Mon Sep 17 00:00:00 2001 From: vanDeventer Date: Tue, 27 May 2025 14:16:35 +0200 Subject: [PATCH 03/81] redo configuration to have configurable services and working on pump station --- leveler/README.md | 37 ++++ leveler/leveler.go | 133 +++++++++++++++ leveler/thing.go | 302 +++++++++++++++++++++++++++++++++ revolutionary/revolutionary.go | 56 +++--- revolutionary/thing.go | 123 ++++++++++---- 5 files changed, 581 insertions(+), 70 deletions(-) create mode 100644 leveler/README.md create mode 100644 leveler/leveler.go create mode 100644 leveler/thing.go diff --git a/leveler/README.md b/leveler/README.md new file mode 100644 index 0000000..49fbb63 --- /dev/null +++ b/leveler/README.md @@ -0,0 +1,37 @@ +# mbaigo System: Leveler + +The Leveler is a distributed control system (DSC) for a pump station (part of an automatic control course's lab) where the aim is to keep the level of an upper tank constant, where the tank is part of a closed (water) circuit. + +It offers three services, *setpoint*, *levelError* and *jitter*. +The setpoint can be read (e.g., GET) or set (e.g., PUT). +The error signal is the difference between the setpoint or desired temperature and the current temperature. +It can only be read. +The jitter is the time it takes to obtain a new temperature reading and setting the new valve position. + +The control loop is executed every 5 seconds, and can be configured. + +## Compiling +The module needs to be initialize in the terminal or command line interface with the command *go.mod* file with ``` go mod init github.com/sdoque/systems/leveler``` before running *go mod tidy*. + +To run the code, one just needs to type in ```go run .``` within a terminal or at a command prompt. + +It is **important** to start the program from within its own directory (and each system should have their own directory) because it looks for its configuration file there. If it does not find it there, it will generate one and shutdown to allow the configuration file to be updated. + +The configuration and operation of the system can be verified using the system's web server using a standard web browser, whose address is provided by the system at startup. + +To build the software for one's own machine, +```go build -o leveler_imac```, where the ending is used to clarify for which platform the code has been compiled for. + + +## Cross compiling/building +The following commands enable one to build for different platforms: +- Intel Mac: ```GOOS=darwin GOARCH=amd64 go build -o leveler_imac leveler.go thing.go``` +- ARM Mac: ```GOOS=darwin GOARCH=arm64 go build -o leveler_amac leveler.go thing.go``` +- Windows 64: ```GOOS=windows GOARCH=amd64 go build -o leveler.exe leveler.go thing.go``` +- Raspberry Pi 64: ```GOOS=linux GOARCH=arm64 go build -o leveler_rpi64 leveler.go thing.go``` +- Linux: ```GOOS=linux GOARCH=amd64 go build -o leveler_linux leveler.go thing.go``` + +One can find a complete list of platform by typing *‌go tool dist list* at the command prompt + +If one wants to secure copy it to a Raspberry pi, +`scp leveler_rpi64 username@ipAddress:mbaigo/leveler/` where user is the *username* @ the *IP address* of the Raspberry Pi with a relative (to the user's home directory) target *mbaigo/leveler/* directory. \ No newline at end of file diff --git a/leveler/leveler.go b/leveler/leveler.go new file mode 100644 index 0000000..3c777fd --- /dev/null +++ b/leveler/leveler.go @@ -0,0 +1,133 @@ +/******************************************************************************* + * Copyright (c) 2024 Synecdoque + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, subject to the following conditions: + * + * The software is licensed under the MIT License. See the LICENSE file in this repository for details. + * + * Contributors: + * Jan A. van Deventer, Luleå - initial implementation + * Thomas Hedeler, Hamburg - initial implementation + ***************************************************************************SDG*/ + +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "time" + + "github.com/sdoque/mbaigo/components" + "github.com/sdoque/mbaigo/usecases" +) + +func main() { + // prepare for graceful shutdown + ctx, cancel := context.WithCancel(context.Background()) // create a context that can be cancelled + defer cancel() // make sure all paths cancel the context to avoid context leak + + // instantiate the System + sys := components.NewSystem("Leveler", ctx) + + // Instantiate the husk + sys.Husk = &components.Husk{ + Description: " is a controller for a consumed servo motor position based on a consumed temperature", + Certificate: "ABCD", + Details: map[string][]string{"Developer": {"Synecdoque"}}, + ProtoPort: map[string]int{"https": 0, "http": 20154, "coap": 0}, + InfoLink: "https://github.com/sdoque/systems/tree/main/leveler", + } + + // instantiate a template unit asset + assetTemplate := initTemplate() + assetName := assetTemplate.GetName() + sys.UAssets[assetName] = &assetTemplate + + // Configure the system + rawResources, err := usecases.Configure(&sys) + if err != nil { + log.Fatalf("Configuration error: %v\n", err) + } + sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) + for _, raw := range rawResources { + var uac usecases.ConfigurableAsset + if err := json.Unmarshal(raw, &uac); err != nil { + log.Fatalf("Resource configuration error: %+v\n", err) + } + ua, cleanup := newResource(uac, &sys) + defer cleanup() + sys.UAssets[ua.GetName()] = &ua + } + + // Generate PKI keys and CSR to obtain a authentication certificate from the CA + usecases.RequestCertificate(&sys) + + // Register the (system) and its services + usecases.RegisterServices(&sys) + + // start the http handler and server + go usecases.SetoutServers(&sys) + + // wait for shutdown signal, and gracefully close properly goroutines with context + <-sys.Sigs // wait for a SIGINT (Ctrl+C) signal + fmt.Println("\nshuting down system", sys.Name) + cancel() // cancel the context, signaling the goroutines to stop + time.Sleep(2 * time.Second) // allow the go routines to be executed, which might take more time than the main routine to end +} + +// Serving handles the resources services. NOTE: it expects those names from the request URL path +func (t *UnitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath string) { + switch servicePath { + case "setpoint": + t.setpt(w, r) + case "levelError": + t.diff(w, r) + case "jitter": + t.variations(w, r) + default: + http.Error(w, "Invalid service request [Do not modify the services subpath in the configuration file]", http.StatusBadRequest) + } +} + +func (rsc *UnitAsset) setpt(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + setPointForm := rsc.getSetPoint() + usecases.HTTPProcessGetRequest(w, r, &setPointForm) + case "PUT": + sig, err := usecases.HTTPProcessSetRequest(w, r) + if err != nil { + log.Println("Error with the setting request of the position ", err) + } + rsc.setSetPoint(sig) + default: + http.Error(w, "Method is not supported.", http.StatusNotFound) + } +} + +func (rsc *UnitAsset) diff(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + signalErr := rsc.getError() + usecases.HTTPProcessGetRequest(w, r, &signalErr) + default: + http.Error(w, "Method is not supported.", http.StatusNotFound) + } +} + +func (rsc *UnitAsset) variations(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + signalErr := rsc.getJitter() + usecases.HTTPProcessGetRequest(w, r, &signalErr) + default: + http.Error(w, "Method is not supported.", http.StatusNotFound) + } +} diff --git a/leveler/thing.go b/leveler/thing.go new file mode 100644 index 0000000..b2c85f2 --- /dev/null +++ b/leveler/thing.go @@ -0,0 +1,302 @@ +/******************************************************************************* + * Copyright (c) 2024 Synecdoque + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, subject to the following conditions: + * + * The software is licensed under the MIT License. See the LICENSE file in this repository for details. + * + * Contributors: + * Jan A. van Deventer, Luleå - initial implementation + * Thomas Hedeler, Hamburg - initial implementation + ***************************************************************************SDG*/ + +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "time" + + "github.com/sdoque/mbaigo/components" + "github.com/sdoque/mbaigo/forms" + "github.com/sdoque/mbaigo/usecases" +) + +// -------------------------------------Define the unit asset +// Traits are Asset-specific configurable parameters and variables +type Traits struct { + SetPt float64 `json:"setPoint"` // the set point for the level + Period time.Duration `json:"samplingPeriod"` + Kp float64 `json:"kp"` + Lambda float64 `json:"lambda"` + Ki float64 `json:"ki"` + jitter time.Duration + deviation float64 + previousT float64 +} + +// UnitAsset type models the unit asset (interface) of the system +type UnitAsset struct { + Name string `json:"name"` + Owner *components.System `json:"-"` + Details map[string][]string `json:"details"` + ServicesMap components.Services `json:"-"` + CervicesMap components.Cervices `json:"-"` + Traits +} + +// GetName returns the name of the Resource. +func (ua *UnitAsset) GetName() string { + return ua.Name +} + +// GetServices returns the services of the Resource. +func (ua *UnitAsset) GetServices() components.Services { + return ua.ServicesMap +} + +// GetCervices returns the list of consumed services by the Resource. +func (ua *UnitAsset) GetCervices() components.Cervices { + return ua.CervicesMap +} + +// GetDetails returns the details of the Resource. +func (ua *UnitAsset) GetDetails() map[string][]string { + return ua.Details +} + +// GetTraits returns the traits of the Resource. +func (ua *UnitAsset) GetTraits() any { + return ua.Traits +} + +// ensure UnitAsset implements components.UnitAsset (this check is done at during the compilation) +var _ components.UnitAsset = (*UnitAsset)(nil) + +//-------------------------------------Instantiate a unit asset template + +// initTemplate initializes a UnitAsset with default values. +func initTemplate() components.UnitAsset { + setPointService := components.Service{ + Definition: "setPoint", + SubPath: "setpoint", + Details: map[string][]string{"Unit": {"Percent"}, "Forms": {"SignalA_v1a"}}, + RegPeriod: 100, + CUnit: "Eur/h", + Description: "provides the current thermal setpoint (GET) or sets it (PUT)", + } + levelErrorService := components.Service{ + Definition: "levelError", + SubPath: "levelerror", + Details: map[string][]string{"Unit": {"Percent"}, "Forms": {"SignalA_v1a"}}, + RegPeriod: 30, + Description: "provides the current difference between the set point and the temperature (GET)", + } + jitterService := components.Service{ + Definition: "jitter", + SubPath: "jitter", + Details: map[string][]string{"Unit": {"millisecond"}, "Forms": {"SignalA_v1a"}}, + RegPeriod: 120, + Description: "provides the current jitter or control algorithm execution calculated every period (GET)", + } + + assetTraits := Traits{ + SetPt: 20, + Period: 5, + Kp: 5, + Lambda: 0.5, + Ki: 0, + } + // var uat components.UnitAsset // this is an interface, which we then initialize + uat := &UnitAsset{ + Name: "Leveler_1", + Details: map[string][]string{"Location": {"UpperTank"}}, + Traits: assetTraits, + ServicesMap: components.Services{ + setPointService.SubPath: &setPointService, + levelErrorService.SubPath: &levelErrorService, + jitterService.SubPath: &jitterService, + }, + } + return uat +} + +//-------------------------------------Instantiate the unit assets based on configuration + +// newResource creates the Resource resource with its pointers and channels based on the configuration using the tConig structs +func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.System) (components.UnitAsset, func()) { + // determine the protocols that the system supports + sProtocols := components.SProtocols(sys.Husk.ProtoPort) + // instantiate the consumed services + t := &components.Cervice{ + Definition: "level", + Protos: sProtocols, + Nodes: make(map[string][]string, 0), + } + + r := &components.Cervice{ + Definition: "pumpSpeed", + Protos: sProtocols, + Nodes: make(map[string][]string, 0), + } + + // instantiate the unit asset + ua := &UnitAsset{ + Name: configuredAsset.Name, + Owner: sys, + Details: configuredAsset.Details, + ServicesMap: usecases.MakeServiceMap(configuredAsset.Services), + CervicesMap: components.Cervices{ + t.Definition: t, + r.Definition: r, + }, + } + + traits, err := UnmarshalTraits(configuredAsset.Traits) + if err != nil { + log.Println("Warning: could not unmarshal traits:", err) + } else if len(traits) > 0 { + ua.Traits = traits[0] // or handle multiple traits if needed + } + + ua.CervicesMap["level"].Details = components.MergeDetails(ua.Details, map[string][]string{"Unit": {"Percent"}, "Forms": {"SignalA_v1a"}}) + ua.CervicesMap["pumpSpeed"].Details = components.MergeDetails(ua.Details, map[string][]string{"Unit": {"Percent"}, "Forms": {"SignalA_v1a"}}) + + // start the unit asset(s) + go ua.feedbackLoop(sys.Ctx) + + return ua, func() { + log.Println("Shutting down thermostat ", ua.Name) + } +} + +func UnmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { + var traitsList []Traits + for _, raw := range rawTraits { + var t Traits + if err := json.Unmarshal(raw, &t); err != nil { + return nil, fmt.Errorf("failed to unmarshal trait: %w", err) + } + traitsList = append(traitsList, t) + } + return traitsList, nil +} + +//-------------------------------------Thing's resource methods + +// getSetPoint fills out a signal form with the current level set point +func (ua *UnitAsset) getSetPoint() (f forms.SignalA_v1a) { + f.NewForm() + f.Value = ua.SetPt + f.Unit = "millimeter" + f.Timestamp = time.Now() + return f +} + +// setSetPoint updates the level set point +func (ua *UnitAsset) setSetPoint(f forms.SignalA_v1a) { + ua.SetPt = f.Value + log.Printf("new set point: %.1f", f.Value) +} + +// getErrror fills out a signal form with the current thermal setpoint and temperature +func (ua *UnitAsset) getError() (f forms.SignalA_v1a) { + f.NewForm() + f.Value = ua.deviation + f.Unit = "millimeter" + f.Timestamp = time.Now() + return f +} + +// getJitter fills out a signal form with the current jitter +func (ua *UnitAsset) getJitter() (f forms.SignalA_v1a) { + f.NewForm() + f.Value = float64(ua.jitter.Milliseconds()) + f.Unit = "millisecond" + f.Timestamp = time.Now() + return f +} + +// feedbackLoop is THE control loop (IPR of the system) +func (ua *UnitAsset) feedbackLoop(ctx context.Context) { + // Initialize a ticker for periodic execution + ticker := time.NewTicker(ua.Period * time.Second) + defer ticker.Stop() + + // start the control loop + for { + select { + case <-ticker.C: + ua.processFeedbackLoop() + case <-ctx.Done(): + return + } + } +} + +// processFeedbackLoop is called to execute the control process +func (ua *UnitAsset) processFeedbackLoop() { + jitterStart := time.Now() + + // get the current level + tf, err := usecases.GetState(ua.CervicesMap["level"], ua.Owner) + if err != nil { + log.Printf("\n unable to obtain a level reading error: %s\n", err) + return + } + // Perform a type assertion to convert the returned Form to SignalA_v1a + tup, ok := tf.(*forms.SignalA_v1a) + if !ok { + log.Println("problem unpacking the level signal form") + return + } + + // perform the control algorithm + ua.deviation = ua.SetPt - tup.Value + output := ua.calculateOutput(ua.deviation) + + // prepare the form to send + var of forms.SignalA_v1a + of.NewForm() + of.Value = output + of.Unit = ua.CervicesMap["pumpSpeed"].Details["Unit"][0] + of.Timestamp = time.Now() + + // pack the new valve state form + op, err := usecases.Pack(&of, "application/json") + if err != nil { + return + } + // send the new valve state request + _, err = usecases.SetState(ua.CervicesMap["pumpSpeed"], ua.Owner, op) + if err != nil { + log.Printf("cannot update valve state: %s\n", err) + return + } + + if tup.Value != ua.previousT { + log.Printf("the level is %.2f mm with an error %.2f°mm and the pumpSpeed set at %.2f%%\n", tup.Value, ua.deviation, output) + ua.previousT = tup.Value + } + + ua.jitter = time.Since(jitterStart) +} + +// calculateOutput is the actual P controller +func (ua *UnitAsset) calculateOutput(thermDiff float64) float64 { + pSpeed := ua.Kp*thermDiff + 50 // if the error is 0, the position is at 50% + + // limit the output between 0 and 100% + if pSpeed < 0 { + pSpeed = 0 + } else if pSpeed > 100 { + pSpeed = 100 + } + return pSpeed +} diff --git a/revolutionary/revolutionary.go b/revolutionary/revolutionary.go index a754fe8..4943a3d 100644 --- a/revolutionary/revolutionary.go +++ b/revolutionary/revolutionary.go @@ -53,17 +53,17 @@ func main() { sys.UAssets[assetName] = &assetTemplate // Configure the system - rawResources, servsTemp, err := usecases.Configure(&sys) + rawResources, err := usecases.Configure(&sys) if err != nil { log.Fatalf("configuration error: %v\n", err) } sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) for _, raw := range rawResources { - var uac UnitAsset + var uac usecases.ConfigurableAsset if err := json.Unmarshal(raw, &uac); err != nil { log.Fatalf("resource configuration error: %+v\n", err) } - ua, cleanup := newResource(uac, &sys, servsTemp) + ua, cleanup := newResource(uac, &sys) defer cleanup() sys.UAssets[ua.GetName()] = &ua } @@ -96,20 +96,20 @@ func (ua *UnitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath // access gets the unit asset's AIO channel datum and sends it in a signal form func (ua *UnitAsset) access(w http.ResponseWriter, r *http.Request) { + requestTray := ServiceTray{ + Sample: make(chan forms.SignalA_v1a), + Error: make(chan error), + } switch r.Method { case "GET": - getMeasuremet := STray{ - Action: "read", - Sample: make(chan forms.SignalA_v1a), - Error: make(chan error), - } - ua.sampleChan <- getMeasuremet + requestTray.Action = "read" // Set the action to read + ua.serviceChannel <- requestTray // Send request to read a signal over the channel select { - case err := <-getMeasuremet.Error: + case err := <-requestTray.Error: fmt.Printf("Logic error in getting measurement, %s\n", err) w.WriteHeader(http.StatusInternalServerError) // Use 500 for an internal error return - case signalForm := <-getMeasuremet.Sample: + case signalForm := <-requestTray.Sample: usecases.HTTPProcessGetRequest(w, r, &signalForm) return case <-time.After(5 * time.Second): // Optional timeout @@ -118,6 +118,7 @@ func (ua *UnitAsset) access(w http.ResponseWriter, r *http.Request) { return } case "POST", "PUT": + requestTray.Action = "write" // Set the action to write contentType := r.Header.Get("Content-Type") mediaType, _, err := mime.ParseMediaType(contentType) @@ -125,7 +126,6 @@ func (ua *UnitAsset) access(w http.ResponseWriter, r *http.Request) { fmt.Println("Error parsing media type:", err) return } - defer r.Body.Close() bodyBytes, err := io.ReadAll(r.Body) if err != nil { @@ -138,45 +138,27 @@ func (ua *UnitAsset) access(w http.ResponseWriter, r *http.Request) { return } - speed, ok := serviceReq.(*forms.SignalA_v1a) + outputForm, ok := serviceReq.(*forms.SignalA_v1a) if !ok { log.Println("problem unpacking the temperature signal form") return } - setSignal := STray{ - Action: "write", - Sample: make(chan forms.SignalA_v1a), - Error: make(chan error), - } + requestTray.Sample <- *outputForm // Send request to add a record to the unit asset - ua.sampleChan <- setSignal + ua.serviceChannel <- requestTray // Use a select statement to wait for responses on either the Result or Error channel select { - case err := <-readRecord.Error: + case err := <-requestTray.Error: if err != nil { log.Printf("Error retrieving service records: %v", err) http.Error(w, "Error retrieving service records", http.StatusInternalServerError) return } - case servvicesList := <-readRecord.Result: - fmt.Println(servvicesList) - var slForm forms.ServiceRecordList_v1 - slForm.NewForm() - slForm.List = servvicesList - updatedRecordBytes, err := usecases.Pack(&slForm, mediaType) - if err != nil { - log.Printf("error confirming new service: %s", err) - http.Error(w, "Error registering service", http.StatusInternalServerError) - } - w.Header().Set("Content-Type", mediaType) - w.WriteHeader(http.StatusOK) - _, err = w.Write([]byte(updatedRecordBytes)) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } + case <-requestTray.Sample: + // Successfully sent the request to the RevPi + w.WriteHeader(http.StatusOK) // Use 200 for a successful request case <-time.After(5 * time.Second): // Optional timeout http.Error(w, "Request timed out", http.StatusGatewayTimeout) log.Println("Failure to process service discovery request") diff --git a/revolutionary/thing.go b/revolutionary/thing.go index 021fb65..24eb024 100644 --- a/revolutionary/thing.go +++ b/revolutionary/thing.go @@ -18,6 +18,7 @@ package main import ( "context" + "encoding/json" "fmt" "log" "os" @@ -28,16 +29,24 @@ import ( "github.com/sdoque/mbaigo/components" "github.com/sdoque/mbaigo/forms" + "github.com/sdoque/mbaigo/usecases" ) // Define the types of requests the measurement manager can handle -type STray struct { +type ServiceTray struct { Action string Sample chan forms.SignalA_v1a Error chan error } -//-------------------------------------Define the unit asset +// -------------------------------------Define the unit asset +// Traits are Asset-specific configurable parameters +type Traits struct { + Address string `json:"address"` // Address of the IO + Value float64 `json:"value"` // Start up value of the IO + MinValue float64 `json:"minValue"` // Minimum value of the IO + MaxValue float64 `json:"maxValue"` // Maximum value of the IO +} // UnitAsset type models the unit asset (interface) of the system. type UnitAsset struct { @@ -46,10 +55,10 @@ type UnitAsset struct { Details map[string][]string `json:"details"` ServicesMap components.Services `json:"-"` CervicesMap components.Cervices `json:"-"` - // - value float64 `json:"-"` - tStamp time.Time `json:"-"` - sampleChan chan STray `json:"-"` // Add a channel for signal reading + // Asset-specific parameters + Traits + tStamp time.Time `json:"-"` + serviceChannel chan ServiceTray `json:"-"` // Add a channel for signal reading } // GetName returns the name of the Resource. @@ -72,6 +81,11 @@ func (ua *UnitAsset) GetDetails() map[string][]string { return ua.Details } +// GetTraits returns the traits of the Resource. +func (ua *UnitAsset) GetTraits() any { + return ua.Traits +} + // ensure UnitAsset implements components.UnitAsset (this check is done at during the compilation) var _ components.UnitAsset = (*UnitAsset)(nil) @@ -81,17 +95,21 @@ var _ components.UnitAsset = (*UnitAsset)(nil) func initTemplate() components.UnitAsset { // Define the services that expose the capabilities of the unit asset(s) access := components.Service{ - Definition: "access", + Definition: "level", SubPath: "access", Details: map[string][]string{"Forms": {"SignalA_v1a"}}, RegPeriod: 30, - Description: "reads the input (GET) or chandes the outup (POST) of the channel", + Description: "reads the input (GET) or changes the output (POST) of the channel", } // var uat components.UnitAsset // this is an interface, which we then initialize uat := &UnitAsset{ - Name: "InputValue_1", - Details: map[string][]string{"Unit": {"Volts"}, "Location": {"UpperTank"}}, + Name: "LevelSensor_1", + Details: map[string][]string{"Unit": {"Percent"}, "Location": {"UpperTank"}, "Description": {"level"}}, + Traits: Traits{ + Address: "InputValue_1", // Default address for the Rev Pi AIO channel + Value: 0.0, // Default value for the output + }, ServicesMap: components.Services{ access.SubPath: &access, // add the service to the map }, @@ -102,13 +120,20 @@ func initTemplate() components.UnitAsset { //-------------------------------------Instantiate the unit assets based on configuration // newResource creates the Resource resource with its pointers and channels based on the configuration -func newResource(uac UnitAsset, sys *components.System, servs []components.Service) (components.UnitAsset, func()) { +func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.System) (components.UnitAsset, func()) { ua := &UnitAsset{ // this a struct that implements the UnitAsset interface - Name: uac.Name, - Owner: sys, - Details: uac.Details, - ServicesMap: components.CloneServices(servs), - sampleChan: make(chan STray), // Initialize the channel + Name: configuredAsset.Name, + Owner: sys, + Details: configuredAsset.Details, + ServicesMap: usecases.MakeServiceMap(configuredAsset.Services), + serviceChannel: make(chan ServiceTray), // Initialize the channel + } + + traits, err := UnmarshalTraits(configuredAsset.Traits) + if err != nil { + log.Println("Warning: could not unmarshal traits:", err) + } else if len(traits) > 0 { + ua.Traits = traits[0] // or handle multiple traits if needed } // start the unit asset(s) @@ -119,11 +144,23 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi } } +func UnmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { + var traitsList []Traits + for _, raw := range rawTraits { + var t Traits + if err := json.Unmarshal(raw, &t); err != nil { + return nil, fmt.Errorf("failed to unmarshal trait: %w", err) + } + traitsList = append(traitsList, t) + } + return traitsList, nil +} + //-------------------------------------Unit asset's functionalities // sampleSignal obtains the temperature from respective Rev Pi AIO resource at regular intervals func (ua *UnitAsset) sampleSignal(ctx context.Context) { - defer close(ua.sampleChan) // Ensure the channel is closed when the goroutine exits + defer close(ua.serviceChannel) // Ensure the channel is closed when the goroutine exits // Create a ticker that triggers every second ticker := time.NewTicker(1 * time.Second) @@ -141,16 +178,17 @@ func (ua *UnitAsset) sampleSignal(ctx context.Context) { return case <-ticker.C: // sample the signal at regular intervals - v, err := readInputVoltage(ua.Name) + v, err := readInputVoltage(ua.Address) if err != nil { fmt.Println("Read error:", err) } else { fmt.Printf("%s = %.2f V\n", ua.Name, v) } + nv := NormalizeToPercent(v, ua.MinValue, ua.MaxValue) // Normalize the value to a percentage - // Send the sampeled signal and timestamp back to the main loop + // Send the sampled signal and timestamp back to the main loop select { - case sigChan <- v: + case sigChan <- nv: tStampChan <- time.Now() case <-ctx.Done(): // Stop the goroutine if context is canceled return @@ -162,28 +200,30 @@ func (ua *UnitAsset) sampleSignal(ctx context.Context) { for { select { case sigValue := <-sigChan: // Update signal value and timestamp - ua.value = sigValue + ua.Value = sigValue ua.tStamp = <-tStampChan - case order := <-ua.sampleChan: + case order := <-ua.serviceChannel: switch order.Action { case "read": // Send the latest signal value and timestamp to the channel var f forms.SignalA_v1a f.NewForm() - f.Value = ua.value - f.Unit = "Volts" + f.Value = ua.Value + f.Unit = "Percent" f.Timestamp = ua.tStamp order.Sample <- f case "write": // Receive the form from the channel f := <-order.Sample - ua.value = f.Value - rawValue := voltageToRaw(ua.value) + ua.Value = f.Value + rawValue := PercentToRaw(ua.Value) err := writeOutput(ua.Name, rawValue) if err != nil { order.Error <- fmt.Errorf("write error: %w", err) + return } + order.Sample <- f // Send the form back to the channel default: order.Error <- fmt.Errorf("invalid action: %s", order.Action) } @@ -219,13 +259,30 @@ func writeOutput(varName string, value int) error { return cmd.Run() } -// voltageToRaw converts a voltage value to a raw value for the piTest command line tool. -func voltageToRaw(voltage float64) int { - if voltage < 0 { - voltage = 0 +// PercentToRaw converts a percentage (0–100%) to a raw 16-bit value for the piTest tool. +func PercentToRaw(percent float64) int { + if percent < 0 { + percent = 0 + } + if percent > 100 { + percent = 100 + } + return int((percent / 100.0) * 65535.0) +} + +// NormalizeToPercent normalizes a reading to a percentage based on the provided min and max values. +func NormalizeToPercent(reading, min, max float64) float64 { + if max == min { + return 0 // or return NaN/error to avoid division by zero + } + percent := 100 * (reading - min) / (max - min) + + // Clamp to [0, 100] in case reading is outside the expected range + if percent < 0 { + return 0 } - if voltage > 10 { - voltage = 10 + if percent > 100 { + return 100 } - return int((voltage / 10.0) * 65535.0) + return percent } From 56a852a683b22808be8f66d18ebac8fe37731ae1 Mon Sep 17 00:00:00 2001 From: vanDeventer Date: Tue, 27 May 2025 17:02:11 +0200 Subject: [PATCH 04/81] upgrading kGrapher --- kgrapher/kgrapher.go | 15 +++++++-- kgrapher/thing.go | 61 +++++++++++++++++++++++++--------- leveler/leveler.go | 10 +++++- revolutionary/revolutionary.go | 9 +++++ revolutionary/thing.go | 1 + 5 files changed, 77 insertions(+), 19 deletions(-) diff --git a/kgrapher/kgrapher.go b/kgrapher/kgrapher.go index 0621033..63e39ad 100644 --- a/kgrapher/kgrapher.go +++ b/kgrapher/kgrapher.go @@ -18,6 +18,7 @@ package main import ( "context" + "crypto/x509/pkix" "encoding/json" "fmt" "log" @@ -43,6 +44,14 @@ func main() { Details: map[string][]string{"Developer": {"Synecdoque"}}, ProtoPort: map[string]int{"https": 0, "http": 20105, "coap": 0}, InfoLink: "https://github.com/sdoque/systems/tree/main/kgrapher", + DName: pkix.Name{ + CommonName: sys.Name, + Organization: []string{"Synecdoque"}, + OrganizationalUnit: []string{"Systems"}, + Locality: []string{"Luleå"}, + Province: []string{"Norrbotten"}, + Country: []string{"SE"}, + }, } // instantiate a template unit asset @@ -51,17 +60,17 @@ func main() { sys.UAssets[assetName] = &assetTemplate // Configure the system - rawResources, servsTemp, err := usecases.Configure(&sys) + rawResources, err := usecases.Configure(&sys) if err != nil { log.Fatalf("Configuration error: %v\n", err) } sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) for _, raw := range rawResources { - var uac UnitAsset + var uac usecases.ConfigurableAsset if err := json.Unmarshal(raw, &uac); err != nil { log.Fatalf("Resource configuration error: %+v\n", err) } - ua, cleanup := newResource(uac, &sys, servsTemp) + ua, cleanup := newResource(uac, &sys) defer cleanup() sys.UAssets[ua.GetName()] = &ua } diff --git a/kgrapher/thing.go b/kgrapher/thing.go index 34b1ba0..e57cf11 100644 --- a/kgrapher/thing.go +++ b/kgrapher/thing.go @@ -19,6 +19,7 @@ package main import ( "bytes" "context" + "encoding/json" "fmt" "io" "log" @@ -32,7 +33,12 @@ import ( "github.com/sdoque/mbaigo/usecases" ) -//-------------------------------------Define the unit asset +// -------------------------------------Define the unit asset +// Traits are Asset-specific configurable parameters +type Traits struct { + SystemList forms.SystemRecordList_v1 `json:"-"` + RepositoryURL string `json:"graphDBurl"` +} // UnitAsset type models the unit asset (interface) of the system type UnitAsset struct { @@ -41,9 +47,8 @@ type UnitAsset struct { Details map[string][]string `json:"details"` ServicesMap components.Services `json:"-"` CervicesMap components.Cervices `json:"-"` - // - SystemList forms.SystemRecordList_v1 `json:"-"` - RepositoryURL string `json:"graphDBurl"` + // Asset-specific parameters + Traits } // GetName returns the name of the Resource. @@ -66,6 +71,11 @@ func (ua *UnitAsset) GetDetails() map[string][]string { return ua.Details } +// GetTraits returns the traits of the Resource. +func (ua *UnitAsset) GetTraits() any { + return ua.Traits +} + // ensure UnitAsset implements components.UnitAsset (this check is done at during the compilation) var _ components.UnitAsset = (*UnitAsset)(nil) @@ -84,11 +94,13 @@ func initTemplate() components.UnitAsset { // var uat components.UnitAsset // this is an interface, which we then initialize uat := &UnitAsset{ - Name: "assembler", - Owner: &components.System{}, - Details: map[string][]string{"Location": {"LocalCloud"}}, - ServicesMap: map[string]*components.Service{cloudgraph.SubPath: &cloudgraph}, - RepositoryURL: "http://localhost:7200/repositories/Arrowhead/statements", + Name: "assembler", + Owner: &components.System{}, + Details: map[string][]string{"Location": {"LocalCloud"}}, + ServicesMap: map[string]*components.Service{cloudgraph.SubPath: &cloudgraph}, + Traits: Traits{ + RepositoryURL: "http://localhost:7200/repositories/Arrowhead/statements", + }, } return uat } @@ -96,14 +108,20 @@ func initTemplate() components.UnitAsset { //-------------------------------------Instantiate unit asset(s) based on configuration // newResource creates the unit asset with its pointers and channels based on the configuration -func newResource(uac UnitAsset, sys *components.System, servs []components.Service) (components.UnitAsset, func()) { +func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.System) (components.UnitAsset, func()) { // var ua components.UnitAsset // this is an interface, which we then initialize ua := &UnitAsset{ // this is an interface, which we then initialize - Name: uac.Name, - Owner: sys, - Details: uac.Details, - ServicesMap: components.CloneServices(servs), - RepositoryURL: uac.RepositoryURL, + Name: configuredAsset.Name, + Owner: sys, + Details: configuredAsset.Details, + ServicesMap: usecases.MakeServiceMap(configuredAsset.Services), + } + + traits, err := UnmarshalTraits(configuredAsset.Traits) + if err != nil { + log.Println("Warning: could not unmarshal traits:", err) + } else if len(traits) > 0 { + ua.Traits = traits[0] // or handle multiple traits if needed } // start the unit asset(s) @@ -113,6 +131,19 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi } } +// UnmarshalTraits unmarshals a slice of json.RawMessage into a slice of Traits. +func UnmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { + var traitsList []Traits + for _, raw := range rawTraits { + var t Traits + if err := json.Unmarshal(raw, &t); err != nil { + return nil, fmt.Errorf("failed to unmarshal trait: %w", err) + } + traitsList = append(traitsList, t) + } + return traitsList, nil +} + // -------------------------------------Unit asset's function methods // assembles ontologies gets the list of systems from the lead registrar and then the ontology of each system diff --git a/leveler/leveler.go b/leveler/leveler.go index 3c777fd..4e5fc94 100644 --- a/leveler/leveler.go +++ b/leveler/leveler.go @@ -18,6 +18,7 @@ package main import ( "context" + "crypto/x509/pkix" "encoding/json" "fmt" "log" @@ -39,10 +40,17 @@ func main() { // Instantiate the husk sys.Husk = &components.Husk{ Description: " is a controller for a consumed servo motor position based on a consumed temperature", - Certificate: "ABCD", Details: map[string][]string{"Developer": {"Synecdoque"}}, ProtoPort: map[string]int{"https": 0, "http": 20154, "coap": 0}, InfoLink: "https://github.com/sdoque/systems/tree/main/leveler", + DName: pkix.Name{ + CommonName: sys.Name, + Organization: []string{"Synecdoque"}, + OrganizationalUnit: []string{"Systems"}, + Locality: []string{"Luleå"}, + Province: []string{"Norrbotten"}, + Country: []string{"SE"}, + }, } // instantiate a template unit asset diff --git a/revolutionary/revolutionary.go b/revolutionary/revolutionary.go index 4943a3d..53e63d3 100644 --- a/revolutionary/revolutionary.go +++ b/revolutionary/revolutionary.go @@ -18,6 +18,7 @@ package main import ( "context" + "crypto/x509/pkix" "encoding/json" "fmt" "io" @@ -45,6 +46,14 @@ func main() { Details: map[string][]string{"Developer": {"Synecdoque"}}, ProtoPort: map[string]int{"https": 0, "http": 20153, "coap": 0}, InfoLink: "https://github.com/sdoque/systems/tree/main/revolutionary", + DName: pkix.Name{ + CommonName: sys.Name, + Organization: []string{"Synecdoque"}, + OrganizationalUnit: []string{"Systems"}, + Locality: []string{"Luleå"}, + Province: []string{"Norrbotten"}, + Country: []string{"SE"}, + }, } // instantiate a template unit asset diff --git a/revolutionary/thing.go b/revolutionary/thing.go index 24eb024..262cfac 100644 --- a/revolutionary/thing.go +++ b/revolutionary/thing.go @@ -144,6 +144,7 @@ func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.Sys } } +// UnmarshalTraits unmarshals a slice of json.RawMessage into a slice of Traits. func UnmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { var traitsList []Traits for _, raw := range rawTraits { From 5a292c4720963628058167c7fce1cb024c86c4bc Mon Sep 17 00:00:00 2001 From: vanDeventer Date: Sat, 7 Jun 2025 09:52:57 +0200 Subject: [PATCH 05/81] adding local ontologies --- Influxer/README.md | 21 ++- Influxer/influxer.go | 19 ++- Influxer/thing.go | 81 ++++++--- kgrapher/README.md | 7 +- kgrapher/kgrapher.go | 18 ++ kgrapher/thing.go | 116 ++++++++++++- leveler/README.md | 12 +- leveler/leveler.go | 4 +- leveler/thing.go | 50 ++++-- photographer/README.md | 28 +++ photographer/photographer.go | 121 +++++++++++++ photographer/thing.go | 159 ++++++++++++++++++ revolutionary/README.md | 4 +- revolutionary/mbaigo System revolutionary.txt | 77 --------- revolutionary/revolutionary.go | 79 ++++----- revolutionary/thing.go | 69 ++++---- 16 files changed, 631 insertions(+), 234 deletions(-) create mode 100644 photographer/README.md create mode 100644 photographer/photographer.go create mode 100644 photographer/thing.go delete mode 100644 revolutionary/mbaigo System revolutionary.txt diff --git a/Influxer/README.md b/Influxer/README.md index e5003a6..8fc1a37 100644 --- a/Influxer/README.md +++ b/Influxer/README.md @@ -1,6 +1,6 @@ -# mbaigo System: influxer +# mbaigo System: Collector -The Influxer is a system that as for asset the time series database [InfluxDB](https://en.wikipedia.org/wiki/InfluxDB). +The Collector is a system that as for asset the time series database [InfluxDB](https://en.wikipedia.org/wiki/InfluxDB). It offers one services, *squery*. squery provides a list of signals present in its bucket’s measurements. @@ -16,29 +16,28 @@ and initialize the *go.mod* file with ``` go mod init github.com/vanDeventer/arr The reason the *go.mod* file is not included in the repository is that when developing the mbaigo module, a replace statement needs to be included to point to the development code. -To run the code, one just needs to type in ```go run influxer.go thing.go``` within a terminal or at a command prompt. +To run the code, one just needs to type in ```go run Collector.go thing.go``` within a terminal or at a command prompt. It is **important** to start the program from within its own directory (and each system should have their own directory) because it looks for its configuration file there. If it does not find it there, it will generate one and shutdown to allow the configuration file to be updated. The configuration and operation of the system can be verified using the system's web server using a standard web browser, whose address is provided by the system at startup. To build the software for one's own machine, -```go build -o influxer```. +```go build -o Collector```. ## Cross compiling/building The following commands enable one to build for different platforms: -- Intel Mac: ```GOOS=darwin GOARCH=amd64 go build -o influxer_imac influxer.go thing.go``` -- ARM Mac: ```GOOS=darwin GOARCH=arm64 go build -o influxer_amac influxer.go thing.go``` -- Windows 64: ```GOOS=windows GOARCH=amd64 go build -o influxer.exe influxer.go thing.go``` -- Raspberry Pi 64: ```GOOS=linux GOARCH=arm64 go build -o influxer_rpi64 influxer.go thing.go``` -- (new) Raspberry Pi 32: ```GOOS=linux GOARCH=arm GOARM=7 go build -o influxer_rpi32 influxer.go thing.go``` -- Linux: ```GOOS=linux GOARCH=amd64 go build -o influxer_linux influxer.go thing.go``` +- Intel Mac: ```GOOS=darwin GOARCH=amd64 go build -o Collector_imac``` +- ARM Mac: ```GOOS=darwin GOARCH=arm64 go build -o Collector_amac ``` +- Windows 64: ```GOOS=windows GOARCH=amd64 go build -o Collector.exe``` +- Raspberry Pi 64: ```GOOS=linux GOARCH=arm64 go build -o Collector_rpi64``` +- Linux: ```GOOS=linux GOARCH=amd64 go build -o Collector_linux``` One can find a complete list of platform by typing *‌go tool dist list* at the command prompt If one wants to secure copy it to a Raspberry pi, -`scp influxer_rpi64 jan@192.168.1.6:Desktop/influxer/` where user is the *username* @ the *IP address* of the Raspberry Pi with a relative (to the user's home directory) target *Desktop/influxer/* directory.influxer +`scp Collector_rpi64 jan@192.168.1.10:rpiExec/Collector/` where user is the *username* @ the *IP address* of the Raspberry Pi with a relative (to the user's home directory) target *rpiExec/Collector/* directory.Collector ## Deployment of the asset diff --git a/Influxer/influxer.go b/Influxer/influxer.go index 1800a8a..aa67f06 100644 --- a/Influxer/influxer.go +++ b/Influxer/influxer.go @@ -18,6 +18,7 @@ package main import ( "context" + "crypto/x509/pkix" "encoding/json" "fmt" "log" @@ -34,14 +35,22 @@ func main() { defer cancel() // make sure all paths cancel the context to avoid context leak // instantiate the System - sys := components.NewSystem("influxer", ctx) + sys := components.NewSystem("Collector", ctx) - // Instatiate the Capusle + // Instantiate the husk sys.Husk = &components.Husk{ Description: " is a system that ingests time signals into an Influx database", Details: map[string][]string{"Developer": {"Synecdoque"}}, ProtoPort: map[string]int{"https": 0, "http": 20180, "coap": 0}, InfoLink: "https://github.com/sdoque/systems/tree/main/influxer", + DName: pkix.Name{ + CommonName: sys.Name, + Organization: []string{"Synecdoque"}, + OrganizationalUnit: []string{"Systems"}, + Locality: []string{"Luleå"}, + Province: []string{"Norrbotten"}, + Country: []string{"SE"}, + }, } // instantiate a template unit asset @@ -50,17 +59,17 @@ func main() { sys.UAssets[assetName] = &assetTemplate // Configure the system - rawResources, servsTemp, err := usecases.Configure(&sys) + rawResources, err := usecases.Configure(&sys) if err != nil { log.Fatalf("Configuration error: %v\n", err) } sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) for _, raw := range rawResources { - var uac UnitAsset + var uac usecases.ConfigurableAsset if err := json.Unmarshal(raw, &uac); err != nil { log.Fatalf("Resource configuration error: %+v\n", err) } - ua, cleanup := newResource(uac, &sys, servsTemp) + ua, cleanup := newResource(uac, &sys) defer cleanup() sys.UAssets[ua.GetName()] = &ua } diff --git a/Influxer/thing.go b/Influxer/thing.go index 0efe769..73b7352 100644 --- a/Influxer/thing.go +++ b/Influxer/thing.go @@ -18,6 +18,7 @@ package main import ( "context" + "encoding/json" "fmt" "log" "net/http" @@ -43,6 +44,15 @@ type MeasurementT struct { //-------------------------------------Define the unit asset +// Traits are Asset-specific configurable parameters +type Traits struct { + FluxURL string `json:"db_url"` + Token string `json:"token"` + Org string `json:"organization"` + Bucket string `json:"bucket"` + Measurements []MeasurementT `json:"measurements"` +} + // UnitAsset type models the unit asset (interface) of the system type UnitAsset struct { Name string `json:"bucket_name"` @@ -51,12 +61,8 @@ type UnitAsset struct { ServicesMap components.Services `json:"-"` CervicesMap components.Cervices `json:"-"` // - FluxURL string `json:"db_url"` - Token string `json:"token"` - Org string `json:"organization"` - Bucket string `json:"bucket"` - Measurements []MeasurementT `json:"measurements"` - client influxdb2.Client // InfluxDB client + Traits + client influxdb2.Client // InfluxDB client } // GetName returns the name of the Resource. @@ -79,6 +85,11 @@ func (ua *UnitAsset) GetDetails() map[string][]string { return ua.Details } +// GetTraits returns the traits of the Resource. +func (ua *UnitAsset) GetTraits() any { + return ua.Traits +} + // ensure UnitAsset implements components.UnitAsset (this check is done at during the compilation) var _ components.UnitAsset = (*UnitAsset)(nil) @@ -98,15 +109,17 @@ func initTemplate() components.UnitAsset { uat := &UnitAsset{ Name: "demo", Details: map[string][]string{"Database": {"InfluxDB"}}, - FluxURL: "http://10.0.0.33:8086", - Token: "K1NTWNlToyUNXdii7IwNJ1W-kMsagUr8w1r4cRVYqK-N-R9vVT1MCJwHFBxOgiW85iKiMSsUpbrxQsQZJA8IzA==", - Org: "mbaigo", - Bucket: "demo", - Measurements: []MeasurementT{ - { - Name: "temperature", - Details: map[string][]string{"Location": {"Kitchen"}}, - Period: 3, + Traits: Traits{ + FluxURL: "http://10.0.0.33:8086", + Token: "K1NTWNlToyUNXdii7IwNJ1W-kMsagUr8w1r4cRVYqK-N-R9vVT1MCJwHFBxOgiW85iKiMSsUpbrxQsQZJA8IzA==", + Org: "mbaigo", + Bucket: "demo", + Measurements: []MeasurementT{ + { + Name: "temperature", + Details: map[string][]string{"Location": {"Kitchen"}}, + Period: 3, + }, }, }, ServicesMap: components.Services{ @@ -119,19 +132,26 @@ func initTemplate() components.UnitAsset { //-------------------------------------Instantiate the unit assets based on configuration // newResource creates a new UnitAsset resource based on the configuration -func newResource(uac UnitAsset, sys *components.System, servs []components.Service) (components.UnitAsset, func()) { +func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.System) (components.UnitAsset, func()) { ua := &UnitAsset{ - Name: uac.Name, + Name: configuredAsset.Name, Owner: sys, - Details: uac.Details, - ServicesMap: components.CloneServices(servs), - FluxURL: uac.FluxURL, - Token: uac.Token, - Org: uac.Org, - Bucket: uac.Bucket, + Details: configuredAsset.Details, + ServicesMap: usecases.MakeServiceMap(configuredAsset.Services), + // FluxURL: uac.FluxURL, + // Token: uac.Token, + // Org: uac.Org, + // Bucket: uac.Bucket, CervicesMap: make(map[string]*components.Cervice), // Initialize map } + traits, err := UnmarshalTraits(configuredAsset.Traits) + if err != nil { + log.Println("Warning: could not unmarshal traits:", err) + } else if len(traits) > 0 { + ua.Traits = traits[0] // or handle multiple traits if needed + } + if ua.FluxURL == "" || ua.Token == "" || ua.Org == "" || ua.Bucket == "" { log.Fatal("Invalid InfluxDB configuration: missing required parameters") } @@ -145,7 +165,7 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi // Collect and ingest measurements var wg sync.WaitGroup sProtocols := components.SProtocols(sys.Husk.ProtoPort) - for _, measurement := range uac.Measurements { + for _, measurement := range ua.Traits.Measurements { // determine the protocols that the system supports cMeasurement := components.Cervice{ Definition: measurement.Name, @@ -173,6 +193,19 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi } } +// UnmarshalTraits unmarshals a slice of json.RawMessage into a slice of Traits. +func UnmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { + var traitsList []Traits + for _, raw := range rawTraits { + var t Traits + if err := json.Unmarshal(raw, &t); err != nil { + return nil, fmt.Errorf("failed to unmarshal trait: %w", err) + } + traitsList = append(traitsList, t) + } + return traitsList, nil +} + //-------------------------------------Unit asset's functionalities // collectIngest diff --git a/kgrapher/README.md b/kgrapher/README.md index 546899a..5522e8a 100644 --- a/kgrapher/README.md +++ b/kgrapher/README.md @@ -10,10 +10,13 @@ Currently, when the KGrapher’s only service is invoked from a web browser, it Using the model in conjunction with the Arrowhead Framework Ontology (afo), a computer can infer new knowledge about the local cloud and reason about it. +The KGrapher also serves (makes available) local ontologies and knowledge graphs. +One can obtain these description through the *localOntologies* service. + ## Compiling To compile the code, one needs initialize the *go.mod* file with ``` go mod init github.com/sdoque/systems/kgrapher``` before running *go mod tidy*. -To run the code, one just needs to type in ```go run kgrapher.go thing.go``` within a terminal or at a command prompt. One can also build it to get an executable of it ```go run modeler.go thing.go``` +To run the code, one just needs to type in ```go run .``` within a terminal or at a command prompt. It is **important** to start the program from within its own directory (and each system should have their own directory) because it looks for its configuration file there. If it does not find it there, it will generate one and shutdown to allow the configuration file to be updated. @@ -26,7 +29,7 @@ To build the software for one's own machine, ## Cross compiling/building The following commands enable one to build for different platforms: -- Raspberry Pi 64: ```GOOS=linux GOARCH=arm64 go build -o kgrapher_rpi64 kgrapher.go thing.go``` +- Raspberry Pi 64: ```GOOS=linux GOARCH=arm64 go build -o kgrapher_rpi64``` One can find a complete list of platform by typing *‌go tool dist list* at the command prompt diff --git a/kgrapher/kgrapher.go b/kgrapher/kgrapher.go index 63e39ad..f209bbe 100644 --- a/kgrapher/kgrapher.go +++ b/kgrapher/kgrapher.go @@ -96,11 +96,16 @@ func (ua *UnitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath switch servicePath { case "cloudgraph": ua.aggregate(w, r) + case "localontologies": + ua.listOntologies(w, r) + case "files": + // this is a catch-all for the files service, which is not implemented in this system default: http.Error(w, "Invalid service request [Do not modify the services subpath in the configuration file]", http.StatusBadRequest) } } +// assembleOntologies writes out the knowledge graph of the local cloud and pushes it to GraphDB func (ua *UnitAsset) aggregate(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": @@ -109,3 +114,16 @@ func (ua *UnitAsset) aggregate(w http.ResponseWriter, r *http.Request) { http.Error(w, "Method is not supported.", http.StatusNotFound) } } + +// listOntologies writes out the HTML produced by localOntologies() +func (ua *UnitAsset) listOntologies(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + p := r.Pattern + html := ua.localOntologies(p) + w.Header().Set("Content-Type", "text/html; charset=utf-8") // set the content type for the response of the HTML page not the ontologies + fmt.Fprint(w, html) + default: + http.Error(w, "Method is not supported.", http.StatusNotFound) + } +} diff --git a/kgrapher/thing.go b/kgrapher/thing.go index e57cf11..9732580 100644 --- a/kgrapher/thing.go +++ b/kgrapher/thing.go @@ -25,6 +25,8 @@ import ( "log" "mime" "net/http" + "os" + "path/filepath" "strings" "time" @@ -38,6 +40,7 @@ import ( type Traits struct { SystemList forms.SystemRecordList_v1 `json:"-"` RepositoryURL string `json:"graphDBurl"` + LOntologies map[string]string `json:"localOntologies"` // map of ontology names to their file paths } // UnitAsset type models the unit asset (interface) of the system @@ -92,14 +95,25 @@ func initTemplate() components.UnitAsset { Description: "provides the knowledge graph of a local cloud (GET)", } + localOntologies := components.Service{ + Definition: "localOntologies", + SubPath: "localontologies", + Details: map[string][]string{"Location": {"LocalCloud"}}, + RegPeriod: 61, + Description: "provides the list of local ontologies (GET)", + } + // var uat components.UnitAsset // this is an interface, which we then initialize uat := &UnitAsset{ Name: "assembler", Owner: &components.System{}, Details: map[string][]string{"Location": {"LocalCloud"}}, - ServicesMap: map[string]*components.Service{cloudgraph.SubPath: &cloudgraph}, + ServicesMap: map[string]*components.Service{cloudgraph.SubPath: &cloudgraph, localOntologies.SubPath: &localOntologies}, Traits: Traits{ RepositoryURL: "http://localhost:7200/repositories/Arrowhead/statements", + LOntologies: map[string]string{ + "alc": "alc-ontology-local.ttl", // Initialize the map for local ontologies + }, }, } return uat @@ -124,14 +138,23 @@ func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.Sys ua.Traits = traits[0] // or handle multiple traits if needed } - // start the unit asset(s) + // Ensure that you have a valid local ontology directory + const dir = "./files" + // 1. Ensure ./files exists + if err := os.MkdirAll(dir, 0755); err != nil { + log.Fatalf("could not create directory %q: %v", dir, err) + } + serverAddress := ua.Owner.Host.IPAddresses[0] // Use the first IP address of the system + ontologyURL := fmt.Sprintf("http://%s:20105/kgrapher/assembler/files/", serverAddress) //only using http for now TODO: use https + // 2. Resolve local ontologies to their full URLs + resolveLocalOntologies(ua.LOntologies, dir, ontologyURL) return ua, func() { log.Println("Disconnecting from GraphDB") } } -// UnmarshalTraits unmarshals a slice of json.RawMessage into a slice of Traits. +// UnmarshalTraits un-marshals a slice of json.RawMessage into a slice of Traits. func UnmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { var traitsList []Traits for _, raw := range rawTraits { @@ -144,6 +167,23 @@ func UnmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { return traitsList, nil } +// resolveLocalOntologies checks if the local ontology files exist in the specified directory. +// If they do, it updates the map with the full URL; if not, it removes the entry and logs a warning. +func resolveLocalOntologies(localOntologies map[string]string, dir string, baseURL string) { + for prefix, filename := range localOntologies { + fullPath := filepath.Join(dir, filename) + + if _, err := os.Stat(fullPath); err == nil { + // File exists: update to full URL + localOntologies[prefix] = baseURL + filename + } else { + // File does not exist: remove entry and warn + fmt.Printf("Warning: ontology file %s not found in %s. Removing prefix '%s'.\n", filename, dir, prefix) + delete(localOntologies, prefix) + } + } +} + // -------------------------------------Unit asset's function methods // assembles ontologies gets the list of systems from the lead registrar and then the ontology of each system @@ -220,7 +260,7 @@ func (ua *UnitAsset) assembleOntologies(w http.ResponseWriter) { return } - // Prepare the local cloud's knowledge graph by asking each system their their knowledeg graph + // Prepare the local cloud's knowledge graph by asking each system their their knowledge graph prefixes := make(map[string]bool) // To store unique prefixes processedBlocks := make(map[string]bool) // To track processed RDF blocks var uniqueIndividuals []string // To store unique RDF individuals @@ -270,14 +310,19 @@ func (ua *UnitAsset) assembleOntologies(w http.ResponseWriter) { // Construct the graph string var graph string + // updatePrefixes(prefixes, ua.Traits.LOntologies) //update prefixes with local ontology URIs TO DO: remove function call, it is not used anymore // Write unique prefixes once for prefix := range prefixes { graph += prefix + "\n" } // Add the ontology definition - rdf := "\n:ontology a owl:Ontology .\n" - graph += rdf + "\n" + ontoImport := "\n:ontology a owl:Ontology " + for _, uri := range ua.Traits.LOntologies { + ontoImport += fmt.Sprintf(";\n owl:imports <%s> ", uri) + } + ontoImport += ".\n" + graph += ontoImport + "\n" // Write unique RDF blocks for _, block := range uniqueIndividuals { @@ -315,3 +360,62 @@ func (ua *UnitAsset) assembleOntologies(w http.ResponseWriter) { fmt.Println("GraphDB Response Body:", string(body)) } } + +// updatePrefix_Target updates the prefixes in the RDF blocks with the new URIs from the local ontologies. +func updatePrefixes(prefixes map[string]bool, prefixUpdates map[string]string) { + updated := make(map[string]bool) + + for line := range prefixes { + if strings.HasPrefix(line, "@prefix") { + parts := strings.Fields(line) + if len(parts) >= 3 { + prefix := strings.TrimSuffix(parts[1], ":") // e.g., "alc" + if newURI, ok := prefixUpdates[prefix]; ok { + // Update the line with the new URI + line = fmt.Sprintf("@prefix %s: <%s#> .", prefix, newURI) + } + } + } + updated[line] = true + } + + // Replace the original map with the updated one + for k := range prefixes { + delete(prefixes, k) + } + for k := range updated { + prefixes[k] = true + } +} + +// ----------- Local Ontologies Service ----------------------------------------------------------- + +// localOntologiesHandler handles requests to the /localontologies endpoint +// localOntologies reads the ./ontologies directory and builds an HTML list +func (ua *UnitAsset) localOntologies(sp string) string { + entries, err := os.ReadDir("./files") + if err != nil { + return fmt.Sprintf("

Error: could not read files directory: %v

", err) + } + + var sb strings.Builder + sb.WriteString(` +Available Ontologies + +

Available Ontologies

+
    `) + + for _, entry := range entries { + if entry.IsDir() { + continue + } + name := entry.Name() + // Foo.owl will now hit your FileServer at "/" and serve ./files/Foo.owl + link := sp + ua.Name + "/files/" + name + sb.WriteString(fmt.Sprintf(`
  • %s
  • `, link, name)) + } + + sb.WriteString(`
+`) + return sb.String() +} diff --git a/leveler/README.md b/leveler/README.md index 49fbb63..67de751 100644 --- a/leveler/README.md +++ b/leveler/README.md @@ -25,13 +25,13 @@ To build the software for one's own machine, ## Cross compiling/building The following commands enable one to build for different platforms: -- Intel Mac: ```GOOS=darwin GOARCH=amd64 go build -o leveler_imac leveler.go thing.go``` -- ARM Mac: ```GOOS=darwin GOARCH=arm64 go build -o leveler_amac leveler.go thing.go``` -- Windows 64: ```GOOS=windows GOARCH=amd64 go build -o leveler.exe leveler.go thing.go``` -- Raspberry Pi 64: ```GOOS=linux GOARCH=arm64 go build -o leveler_rpi64 leveler.go thing.go``` -- Linux: ```GOOS=linux GOARCH=amd64 go build -o leveler_linux leveler.go thing.go``` +- Intel Mac: ```GOOS=darwin GOARCH=amd64 go build -o leveler_imac ``` +- ARM Mac: ```GOOS=darwin GOARCH=arm64 go build -o leveler_amac``` +- Windows 64: ```GOOS=windows GOARCH=amd64 go build -o leveler.exe ``` +- Raspberry Pi 64: ```GOOS=linux GOARCH=arm64 go build -o leveler_rpi64``` +- Linux: ```GOOS=linux GOARCH=amd64 go build -o leveler_linux ``` One can find a complete list of platform by typing *‌go tool dist list* at the command prompt If one wants to secure copy it to a Raspberry pi, -`scp leveler_rpi64 username@ipAddress:mbaigo/leveler/` where user is the *username* @ the *IP address* of the Raspberry Pi with a relative (to the user's home directory) target *mbaigo/leveler/* directory. \ No newline at end of file +`scp leveler_rpi64 pi@192.168.1.9:station/leveler/` where user is the *pi* @ the *IP address* of the Raspberry Pi with a relative (to the user's home directory) target *station/leveler/* directory. \ No newline at end of file diff --git a/leveler/leveler.go b/leveler/leveler.go index 4e5fc94..50ae1d9 100644 --- a/leveler/leveler.go +++ b/leveler/leveler.go @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2024 Synecdoque + * Copyright (c) 2025 Synecdoque * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -95,7 +95,7 @@ func (t *UnitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath switch servicePath { case "setpoint": t.setpt(w, r) - case "levelError": + case "levelerror": t.diff(w, r) case "jitter": t.variations(w, r) diff --git a/leveler/thing.go b/leveler/thing.go index b2c85f2..c2243e9 100644 --- a/leveler/thing.go +++ b/leveler/thing.go @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2024 Synecdoque + * Copyright (c) 2025 Synecdoque * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,6 +21,7 @@ import ( "encoding/json" "fmt" "log" + "math" "time" "github.com/sdoque/mbaigo/components" @@ -38,7 +39,8 @@ type Traits struct { Ki float64 `json:"ki"` jitter time.Duration deviation float64 - previousT float64 + integral float64 + previousT float64 // previous level reading to avoid flooding the log } // UnitAsset type models the unit asset (interface) of the system @@ -165,7 +167,7 @@ func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.Sys ua.Traits = traits[0] // or handle multiple traits if needed } - ua.CervicesMap["level"].Details = components.MergeDetails(ua.Details, map[string][]string{"Unit": {"Percent"}, "Forms": {"SignalA_v1a"}}) + ua.CervicesMap["level"].Details = components.MergeDetails(ua.Details, map[string][]string{"Unit": {"Percent"}, "Forms": {"SignalA_v1a"}, "Location": {"UpperTank"}}) ua.CervicesMap["pumpSpeed"].Details = components.MergeDetails(ua.Details, map[string][]string{"Unit": {"Percent"}, "Forms": {"SignalA_v1a"}}) // start the unit asset(s) @@ -194,7 +196,7 @@ func UnmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { func (ua *UnitAsset) getSetPoint() (f forms.SignalA_v1a) { f.NewForm() f.Value = ua.SetPt - f.Unit = "millimeter" + f.Unit = "Percent" f.Timestamp = time.Now() return f } @@ -209,7 +211,7 @@ func (ua *UnitAsset) setSetPoint(f forms.SignalA_v1a) { func (ua *UnitAsset) getError() (f forms.SignalA_v1a) { f.NewForm() f.Value = ua.deviation - f.Unit = "millimeter" + f.Unit = "Percent" f.Timestamp = time.Now() return f } @@ -268,20 +270,20 @@ func (ua *UnitAsset) processFeedbackLoop() { of.Unit = ua.CervicesMap["pumpSpeed"].Details["Unit"][0] of.Timestamp = time.Now() - // pack the new valve state form + // pack the new pumpSpeed state form op, err := usecases.Pack(&of, "application/json") if err != nil { return } - // send the new valve state request + // send the new state request _, err = usecases.SetState(ua.CervicesMap["pumpSpeed"], ua.Owner, op) if err != nil { - log.Printf("cannot update valve state: %s\n", err) + log.Printf("cannot update pump speed: %s\n", err) return } if tup.Value != ua.previousT { - log.Printf("the level is %.2f mm with an error %.2f°mm and the pumpSpeed set at %.2f%%\n", tup.Value, ua.deviation, output) + log.Printf("the level is %.2f percent with an error %.2f percent and the pumpSpeed set at %.2f%%\n", tup.Value, ua.deviation, output) ua.previousT = tup.Value } @@ -289,14 +291,26 @@ func (ua *UnitAsset) processFeedbackLoop() { } // calculateOutput is the actual P controller -func (ua *UnitAsset) calculateOutput(thermDiff float64) float64 { - pSpeed := ua.Kp*thermDiff + 50 // if the error is 0, the position is at 50% - - // limit the output between 0 and 100% - if pSpeed < 0 { - pSpeed = 0 - } else if pSpeed > 100 { - pSpeed = 100 +func (ua *UnitAsset) calculateOutput(levelDiff float64) float64 { + // Proportional term + pTerm := ua.Kp * levelDiff + + // Update integral with exponential decay using Lambda + sampleSeconds := (ua.Period * time.Second).Seconds() + decay := math.Exp(-sampleSeconds / ua.Lambda) + ua.integral = decay*ua.integral + levelDiff*sampleSeconds + + // Integral term + iTerm := ua.Ki * ua.integral + + // Combined PI output + output := pTerm + iTerm + + // Limit output to [0, 100]% + if output < 0 { + output = 0 + } else if output > 100 { + output = 100 } - return pSpeed + return output } diff --git a/photographer/README.md b/photographer/README.md new file mode 100644 index 0000000..0fc0d22 --- /dev/null +++ b/photographer/README.md @@ -0,0 +1,28 @@ +# # mbaigo System: Photographer + +The photographer system takes pictures using connected camera as a service. +The take picture service stores the file locally and provides a link to get the file. + +## Compiling +To compile the code, one needs to initialize the *go.mod* file with ``` go mod init github.com/sdoque/photographer``` before running *go mod tidy*. + +The reason the *go.mod* file is not included in the repository is that when developing the mbaigo module, a replace statement needs to be included to point to the development code. + +To run the code, one just needs to type in ```go run .``` within a terminal or at a command prompt. + +It is **important** to start the program from within it own directory (and each system should have their own directory) because it looks for its configuration file there. If it does not find it there, it will generate one and shutdown to allow the configuration file to be updated. + +The configuration and operation of the system can be verified using the system's web server using a standard web browser, whose address is provided by the system at startup. + +To build the software for one's own machine, +```go build -o parallax_imac```, where the ending is used to clarify for which platform the code is for. + + +## Cross compiling/building +The following commands enable one to build for different platforms: +- Raspberry Pi 64: ```GOOS=linux GOARCH=arm64 go build -o photographer_rpi64``` + +One can find a complete list of platform by typing *‌go tool dist list* at the command prompt + +If one wants to secure copy it to a Raspberry pi, +`scp parallax_rpi64 jan@192.168.1.6:Desktop/photographer/` where user is the *username* @ the *IP address* of the Raspberry Pi with a relative (to the user's home directory) target *Desktop/photographer/* directory.photographer \ No newline at end of file diff --git a/photographer/photographer.go b/photographer/photographer.go new file mode 100644 index 0000000..b0c6f63 --- /dev/null +++ b/photographer/photographer.go @@ -0,0 +1,121 @@ +/******************************************************************************* + * Copyright (c) 2025 Synecdoque + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, subject to the following conditions: + * + * The software is licensed under the MIT License. See the LICENSE file in this repository for details. + * + * Contributors: + * Jan A. van Deventer, Luleå - initial implementation + * Thomas Hedeler, Hamburg - initial implementation + ***************************************************************************SDG*/ + +package main + +import ( + "context" + "crypto/x509/pkix" + "encoding/json" + "fmt" + "log" + "net/http" + "time" + + "github.com/sdoque/mbaigo/components" + "github.com/sdoque/mbaigo/usecases" +) + +func main() { + // prepare for graceful shutdown + ctx, cancel := context.WithCancel(context.Background()) // create a context that can be cancelled + defer cancel() + + // instantiate the System + sys := components.NewSystem("photographer", ctx) + + // instatiate the husk + sys.Husk = &components.Husk{ + Description: " takes a picture using a camera and saves a file", + Details: map[string][]string{"Developer": {"Arrowhead"}}, + ProtoPort: map[string]int{"https": 0, "http": 20160, "coap": 0}, + InfoLink: "https://github.com/sdoque/mbaigo/tree/master/photographer", + DName: pkix.Name{ + CommonName: sys.Name, + Organization: []string{"Synecdoque"}, + OrganizationalUnit: []string{"Systems"}, + Locality: []string{"Luleå"}, + Province: []string{"Norrbotten"}, + Country: []string{"SE"}, + }, + } + + // instantiate a template unit asset + assetTemplate := initTemplate() + assetName := assetTemplate.GetName() + sys.UAssets[assetName] = &assetTemplate + + // Configure the system + rawResources, err := usecases.Configure(&sys) + if err != nil { + log.Fatalf("Configuration error: %v\n", err) + } + sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) + for _, raw := range rawResources { + var uac usecases.ConfigurableAsset + if err := json.Unmarshal(raw, &uac); err != nil { + log.Fatalf("Resource configuration error: %+v\n", err) + } + ua, cleanup := newResource(uac, &sys) + defer cleanup() + sys.UAssets[ua.GetName()] = &ua + } + + // Generate PKI keys and CSR to obtain a authentication certificate from the CA + usecases.RequestCertificate(&sys) + + // Register the (system) and its services + usecases.RegisterServices(&sys) + + // start the requests handlers and servers + go usecases.SetoutServers(&sys) + + // wait for shutdown signal, and gracefully close properly goroutines with context + <-sys.Sigs // wait for a SIGINT (Ctrl+C) signal + fmt.Println("\nshuting down system", sys.Name) + cancel() // cancel the context, signaling the goroutines to stop + time.Sleep(3 * time.Second) // allow the go routines to be executed, which might take more time than the main routine to end +} + +// Serving handles the resources services. NOTE: it expects those names from the request URL path +func (ua *UnitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath string) { + switch servicePath { + case "photograph": + ua.photograph(w, r) + case "files": + // return a 200 OK acknowledgment + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, "OK") + default: + http.Error(w, "Invalid service request [Do not modify the services subpath in the configuration file]", http.StatusBadRequest) + } +} + +func (ua *UnitAsset) photograph(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + fileForm, err := ua.takePicture() + if err != nil { + log.Println(err) + http.Error(w, "Failed to take a picture, check if PiCam is connected", http.StatusNotFound) + return + } + usecases.HTTPProcessGetRequest(w, r, &fileForm) + + default: + http.Error(w, "Method is not supported.", http.StatusNotFound) + } +} diff --git a/photographer/thing.go b/photographer/thing.go new file mode 100644 index 0000000..20449db --- /dev/null +++ b/photographer/thing.go @@ -0,0 +1,159 @@ +/******************************************************************************* + * Copyright (c) 2025 Synecdoque + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, subject to the following conditions: + * + * The software is licensed under the MIT License. See the LICENSE file in this repository for details. + * + * Contributors: + * Jan A. van Deventer, Luleå - initial implementation + * Thomas Hedeler, Hamburg - initial implementation + ***************************************************************************SDG*/ + +package main + +import ( + "encoding/json" + "fmt" + "log" + "os" + "os/exec" + "strconv" + "time" + + "github.com/sdoque/mbaigo/components" + "github.com/sdoque/mbaigo/forms" + "github.com/sdoque/mbaigo/usecases" +) + +// -------------------------------------Define the unit asset +// Traits are Asset-specific configurable parameters and variables +type Traits struct { +} + +// UnitAsset type models the unit asset (interface) of the system +type UnitAsset struct { + Name string `json:"name"` + Owner *components.System `json:"-"` + Details map[string][]string `json:"details"` + ServicesMap components.Services `json:"-"` + CervicesMap components.Cervices `json:"-"` + Traits +} + +// GetName returns the name of the Resource. +func (ua *UnitAsset) GetName() string { + return ua.Name +} + +// GetServices returns the services of the Resource. +func (ua *UnitAsset) GetServices() components.Services { + return ua.ServicesMap +} + +// GetCervices returns the list of consumed services by the Resource. +func (ua *UnitAsset) GetCervices() components.Cervices { + return ua.CervicesMap +} + +// GetDetails returns the details of the Resource. +func (ua *UnitAsset) GetDetails() map[string][]string { + return ua.Details +} + +// GetTraits returns the traits of the Resource. +func (ua *UnitAsset) GetTraits() any { + return ua.Traits +} + +// ensure UnitAsset implements components.UnitAsset (this check is done at during the compilation) +var _ components.UnitAsset = (*UnitAsset)(nil) + +//-------------------------------------Instantiate a unit asset template + +// initTemplate initializes a UnitAsset with default values. +func initTemplate() components.UnitAsset { + // Define the services that expose the capabilities of the unit asset(s) + photograph := components.Service{ + Definition: "photograph", + SubPath: "photograph", + Details: map[string][]string{"Forms": {"jpeg_v1a"}}, + Description: " takes a picture (GET) and saves it as a file", + } + + // var uat components.UnitAsset // this is an interface, which we then initialize + uat := &UnitAsset{ + Name: "PiCam", + Details: map[string][]string{"Model": {"PiCam v2"}, "Location": {"Entrance"}}, + ServicesMap: components.Services{ + photograph.SubPath: &photograph, // Inline assignment of the temperature service + }, + } + return uat +} + +//-------------------------------------Instantiate the unit assets based on configuration + +// newResource creates the Resource resource with its pointers and channels based on the configuration +func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.System) (components.UnitAsset, func()) { + ua := &UnitAsset{ // this a struct that implements the UnitAsset interface + Name: configuredAsset.Name, + Owner: sys, + Details: configuredAsset.Details, + ServicesMap: usecases.MakeServiceMap(configuredAsset.Services), + } + traits, err := UnmarshalTraits(configuredAsset.Traits) + if err != nil { + log.Println("Warning: could not unmarshal traits:", err) + } else if len(traits) > 0 { + ua.Traits = traits[0] // or handle multiple traits if needed + } + + return ua, func() { + log.Println("disconnecting from sensors") + } +} + +// UnmarshalTraits unmarshals a slice of json.RawMessage into a slice of Traits. +func UnmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { + var traitsList []Traits + for _, raw := range rawTraits { + var t Traits + if err := json.Unmarshal(raw, &t); err != nil { + return nil, fmt.Errorf("failed to unmarshal trait: %w", err) + } + traitsList = append(traitsList, t) + } + return traitsList, nil +} + +//-------------------------------------Unit asset's resource functions + +func (ua *UnitAsset) takePicture() (f forms.FileForm_v1, err error) { + // Ensure the ./pictures directory exists + err = os.MkdirAll("./files", os.ModePerm) + if err != nil { + return f, fmt.Errorf("failed to create directory: %w", err) + } + + // Format the timestamp and create the filename with the ./pictures directory prefix + timestamp := time.Now().Format("20060102-150405") + filename := fmt.Sprintf("/files/image_%s.jpg", timestamp) + + // Command to take a picture + cmd := exec.Command("libcamera-still", "-o", "."+filename) + + // Execute the command + if err := cmd.Run(); err != nil { + return f, fmt.Errorf("failed to take picture: %w", err) + } + urlPath := "http://" + ua.Owner.Host.IPAddresses[0] + ":" + strconv.Itoa(int(ua.Owner.Husk.ProtoPort["http"])) + "/" + ua.Owner.Name + "/" + ua.Name + f.NewForm() + f.FileURL = urlPath + filename + f.Timestamp = time.Now() + return f, nil +} diff --git a/revolutionary/README.md b/revolutionary/README.md index 677bd48..258dd40 100644 --- a/revolutionary/README.md +++ b/revolutionary/README.md @@ -75,7 +75,7 @@ The configuration and operation of the system can be verified using the system's ## Cross compiling/building The following commands enable one to build for a different platform: -- Raspberry Pi 64: ```GOOS=linux GOARCH=arm64 go build -o Revolutionary_rpi64 ``` +- Raspberry Pi 64: ```GOOS=linux GOARCH=arm64 go build -o revolutionary_rpi64``` If one wants to secure copy it to a Revolution pi, -`scp Revolutionary_rpi64 username@ipAddress:mbaigo/Revolutionary/` where user is the *username* @ the *IP address* of the Raspberry Pi with a relative (to the user's home directory) destination (the *mbaigo/Revolutionary/* directory in this case). +```scp revolutionary_rpi64 pi@192.168.1.9:station/revolutionary/``` where user is the *pi* @ the *IP address* of the Raspberry Pi with a relative (to the user's home directory) destination (the *station/Revolutionary/* directory in this case). diff --git a/revolutionary/mbaigo System revolutionary.txt b/revolutionary/mbaigo System revolutionary.txt deleted file mode 100644 index 4253e67..0000000 --- a/revolutionary/mbaigo System revolutionary.txt +++ /dev/null @@ -1,77 +0,0 @@ -# mbaigo System: revolutionary - -## Purpose -The revolutionary system runs on [Revolution Pi Connect 4](https://revolutionpi.com/documentation/revpi-connect-4/) PLC with an analogue input/output module within a pump station demonstrator. -It measures the level of two upper tanks (-10 to +10 Volts) and outputs a control signal to the pump (0 to +10 Volts). - - -## Sequence of events -This system offers three services: the two level sensors and the pump speed signal. -For the services to be discoverable, they must be in the registry of currently available services. -The consuming system asks the Orchestrator for the desired service by describing it. -The orchestrator inquires with the Service Registrar if the service is available and if so provides the service URL. -The consuming system will request the service directly from the Revolutionary system via this URL until it stops working. - -```mermaid -sequenceDiagram - -participant ServiceRegistrar as ServiceRegistrar -participant Orchestrator as Orchestrator -participant Revolutionary as Revolutionary -participant ArrowheadSystem as Any Arrowhead System - -loop Before registration expiration - activate Revolutionary - Revolutionary->>+ServiceRegistrar: Register each service - ServiceRegistrar-->>-Revolutionary: New expiration time - deactivate Revolutionary -end - - -loop Every x period - activate ArrowheadSystem - - alt Service location is unknown - ArrowheadSystem->>+Orchestrator: Discover service provider - activate Orchestrator - Orchestrator->>+ServiceRegistrar: Query for service - ServiceRegistrar-->>-Orchestrator: Return service location - Orchestrator-->>ArrowheadSystem: Forward service location - deactivate Orchestrator - else Service location is known - Note over ArrowheadSystem: Use cached location - end - - loop For each wanted service - ArrowheadSystem->>Revolutionary: Get statistics from service - activate Revolutionary - Revolutionary-->>ArrowheadSystem: Return latest data - deactivate Revolutionary - ArrowheadSystem->>ArrowheadSystem: Cache sampled data - end - - deactivate ArrowheadSystem -end -``` - -## Configuration -The default configuration using the [Rev Pi AIO](https://revolutionpi.com/documentation/revpi-aio/) module is the upper tank level sensor (v+ @ pin 28, GND @ pin 24), middle tank level sensor (v+ @ pin 17, GND @ pin 23). The (output) pump signal is on v+ @ pin 1 with GND @ pin 5. - -## Compiling -To compile the code, one needs to get the mbaigo module -```go get github.com/sdoque/mbaigo``` -and initialize the *go.mod* file with ``` go mod init github.com/sdoque/systems/Revolutionary``` before running *go mod tidy*. - -To run the code, one just needs to type in ```go run .``` within a terminal or at a command prompt. (But that works only on a Revolution Pi, since it is accessing that hardware.) - -It is **important** to start the program from within its own directory (and each system should have their own directory) because program looks for its configuration file there. If it does not find it there, it will generate one and shutdown to allow the configuration file to be updated. - -The configuration and operation of the system can be verified using the system's web server using a standard web browser, whose address is provided by the system at startup. - -## Cross compiling/building -The following commands enable one to build for a different platform: - -- Raspberry Pi 64: ```GOOS=linux GOARCH=arm64 go build -o Revolutionary_rpi64 ``` - -If one wants to secure copy it to a Revolution pi, -`scp Revolutionary_rpi64 username@ipAddress:mbaigo/Revolutionary/` where user is the *username* @ the *IP address* of the Raspberry Pi with a relative (to the user's home directory) destination (the *mbaigo/Revolutionary/* directory in this case). diff --git a/revolutionary/revolutionary.go b/revolutionary/revolutionary.go index 53e63d3..27071fb 100644 --- a/revolutionary/revolutionary.go +++ b/revolutionary/revolutionary.go @@ -20,7 +20,6 @@ import ( "context" "crypto/x509/pkix" "encoding/json" - "fmt" "io" "log" "mime" @@ -67,13 +66,15 @@ func main() { log.Fatalf("configuration error: %v\n", err) } sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) + var cleanups []func() for _, raw := range rawResources { var uac usecases.ConfigurableAsset if err := json.Unmarshal(raw, &uac); err != nil { log.Fatalf("resource configuration error: %+v\n", err) } ua, cleanup := newResource(uac, &sys) - defer cleanup() + cleanups = append(cleanups, cleanup) + // defer cleanup() sys.UAssets[ua.GetName()] = &ua } @@ -89,7 +90,10 @@ func main() { // wait for shutdown signal, and gracefully close properly goroutines with context <-sys.Sigs // wait for a SIGINT (Ctrl+C) signal log.Println("\nshuting down system", sys.Name) - cancel() // cancel the context, signaling the goroutines to stop + cancel() // cancel the context, signaling the goroutines to stop + for _, cleanup := range cleanups { + cleanup() + } time.Sleep(2 * time.Second) // allow the go routines to be executed, which might take more time than the main routine to end } @@ -105,75 +109,62 @@ func (ua *UnitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath // access gets the unit asset's AIO channel datum and sends it in a signal form func (ua *UnitAsset) access(w http.ResponseWriter, r *http.Request) { - requestTray := ServiceTray{ - Sample: make(chan forms.SignalA_v1a), - Error: make(chan error), - } + switch r.Method { - case "GET": - requestTray.Action = "read" // Set the action to read - ua.serviceChannel <- requestTray // Send request to read a signal over the channel + case http.MethodGet: + // Prepare a fresh tray for this request + requestTray := ServiceTray{ + SampledDatum: make(chan forms.SignalA_v1a), + Error: make(chan error), + } + ua.serviceChannel <- requestTray select { case err := <-requestTray.Error: - fmt.Printf("Logic error in getting measurement, %s\n", err) - w.WriteHeader(http.StatusInternalServerError) // Use 500 for an internal error + log.Printf("Logic error in getting measurement: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) return - case signalForm := <-requestTray.Sample: + case signalForm := <-requestTray.SampledDatum: usecases.HTTPProcessGetRequest(w, r, &signalForm) return - case <-time.After(5 * time.Second): // Optional timeout + case <-time.After(5 * time.Second): http.Error(w, "Request timed out", http.StatusGatewayTimeout) - log.Println("Failure to process signal reading request") + log.Println("Timeout on GET access") return } - case "POST", "PUT": - requestTray.Action = "write" // Set the action to write + case http.MethodPost, http.MethodPut: + // Unpack the incoming form + log.Printf("Unpacking output signal form for %s", ua.Name) contentType := r.Header.Get("Content-Type") mediaType, _, err := mime.ParseMediaType(contentType) if err != nil { - fmt.Println("Error parsing media type:", err) + log.Printf("Error parsing media type: %v", err) + http.Error(w, "Unsupported Media Type", http.StatusUnsupportedMediaType) return } - defer r.Body.Close() bodyBytes, err := io.ReadAll(r.Body) if err != nil { - log.Printf("error reading service discovery request body: %v", err) + log.Printf("Error reading request body: %v", err) + http.Error(w, "Bad request", http.StatusBadRequest) return } serviceReq, err := usecases.Unpack(bodyBytes, mediaType) if err != nil { - log.Printf("error extracting the service discovery request %v\n", err) + log.Printf("Error unpacking output signal form: %v", err) + http.Error(w, "Bad request", http.StatusBadRequest) return } - - outputForm, ok := serviceReq.(*forms.SignalA_v1a) + outputForm, ok := serviceReq.(*forms.SignalA_v1a) // Ensure the form is of the expected type if !ok { - log.Println("problem unpacking the temperature signal form") + log.Println("Unexpected form type in access") + http.Error(w, "Bad request", http.StatusBadRequest) return } - requestTray.Sample <- *outputForm - // Send request to add a record to the unit asset - ua.serviceChannel <- requestTray + ua.outputChannel <- outputForm.Value // Send the value to the output channel for processing + w.WriteHeader(http.StatusOK) // Respond with 200 OK if the write is successful - // Use a select statement to wait for responses on either the Result or Error channel - select { - case err := <-requestTray.Error: - if err != nil { - log.Printf("Error retrieving service records: %v", err) - http.Error(w, "Error retrieving service records", http.StatusInternalServerError) - return - } - case <-requestTray.Sample: - // Successfully sent the request to the RevPi - w.WriteHeader(http.StatusOK) // Use 200 for a successful request - case <-time.After(5 * time.Second): // Optional timeout - http.Error(w, "Request timed out", http.StatusGatewayTimeout) - log.Println("Failure to process service discovery request") - return - } default: - http.Error(w, "Method is not supported.", http.StatusNotFound) + http.Error(w, "Method not supported", http.StatusMethodNotAllowed) } } diff --git a/revolutionary/thing.go b/revolutionary/thing.go index 262cfac..80777fe 100644 --- a/revolutionary/thing.go +++ b/revolutionary/thing.go @@ -34,9 +34,8 @@ import ( // Define the types of requests the measurement manager can handle type ServiceTray struct { - Action string - Sample chan forms.SignalA_v1a - Error chan error + SampledDatum chan forms.SignalA_v1a + Error chan error } // -------------------------------------Define the unit asset @@ -59,6 +58,7 @@ type UnitAsset struct { Traits tStamp time.Time `json:"-"` serviceChannel chan ServiceTray `json:"-"` // Add a channel for signal reading + outputChannel chan float64 `json:"-"` // Channel for output signals } // GetName returns the name of the Resource. @@ -127,6 +127,7 @@ func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.Sys Details: configuredAsset.Details, ServicesMap: usecases.MakeServiceMap(configuredAsset.Services), serviceChannel: make(chan ServiceTray), // Initialize the channel + outputChannel: make(chan float64), // Initialize the output channel } traits, err := UnmarshalTraits(configuredAsset.Traits) @@ -141,6 +142,8 @@ func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.Sys return ua, func() { log.Printf("disconnecting from %s\n", ua.Name) + // close(ua.outputChannel) // Ensure the output channel is closed when the goroutine exits + // close(ua.serviceChannel) // Ensure the channel is closed when the goroutine exits } } @@ -161,8 +164,6 @@ func UnmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { // sampleSignal obtains the temperature from respective Rev Pi AIO resource at regular intervals func (ua *UnitAsset) sampleSignal(ctx context.Context) { - defer close(ua.serviceChannel) // Ensure the channel is closed when the goroutine exits - // Create a ticker that triggers every second ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() // Clean up the ticker when done @@ -183,7 +184,7 @@ func (ua *UnitAsset) sampleSignal(ctx context.Context) { if err != nil { fmt.Println("Read error:", err) } else { - fmt.Printf("%s = %.2f V\n", ua.Name, v) + fmt.Printf("%s = %.2f V\n", ua.Name, v/1000) } nv := NormalizeToPercent(v, ua.MinValue, ua.MaxValue) // Normalize the value to a percentage @@ -203,30 +204,24 @@ func (ua *UnitAsset) sampleSignal(ctx context.Context) { case sigValue := <-sigChan: // Update signal value and timestamp ua.Value = sigValue ua.tStamp = <-tStampChan - case order := <-ua.serviceChannel: - switch order.Action { - case "read": - // Send the latest signal value and timestamp to the channel - var f forms.SignalA_v1a - f.NewForm() - f.Value = ua.Value - f.Unit = "Percent" - f.Timestamp = ua.tStamp - order.Sample <- f - case "write": - // Receive the form from the channel - f := <-order.Sample - ua.Value = f.Value - rawValue := PercentToRaw(ua.Value) - err := writeOutput(ua.Name, rawValue) - if err != nil { - order.Error <- fmt.Errorf("write error: %w", err) - return - } - order.Sample <- f // Send the form back to the channel - default: - order.Error <- fmt.Errorf("invalid action: %s", order.Action) + // switch order.Action { + // case "read": + // Send the latest signal value and timestamp to the channel + var f forms.SignalA_v1a + f.NewForm() + f.Value = ua.Value + f.Unit = "Percent" + f.Timestamp = ua.tStamp + order.SampledDatum <- f + case requestedOutup := <-ua.outputChannel: + log.Printf("Received output request for %s: %.2f%%\n", ua.Name, requestedOutup) + rawValue := PercentToRaw(requestedOutup) + log.Printf("Converted output value to raw: %d\n", rawValue) + err := writeOutput(ua.Address, rawValue) + if err != nil { + fmt.Printf("Error writing output: %v\n", err) + return } } } @@ -237,18 +232,18 @@ func readInputVoltage(varName string) (float64, error) { fmt.Println("Reading input:", varName) cmd := exec.Command("/usr/bin/piTest", "-1", "-q", "-r", varName) cmd.Stderr = os.Stderr - output, err := cmd.Output() + reading, err := cmd.Output() if err != nil { return 0, fmt.Errorf("reading the Rev Pi failed: %w", err) } - valueStr := strings.TrimSpace(string(output)) + valueStr := strings.TrimSpace(string(reading)) raw, err := strconv.Atoi(valueStr) if err != nil { return 0, fmt.Errorf("invalid raw value: %w", err) } - voltage := (float64(raw)/65535.0)*20.0 - 10.0 + voltage := float64(raw) // the raw value is in millivolts, convert to volts return voltage, nil } @@ -268,15 +263,15 @@ func PercentToRaw(percent float64) int { if percent > 100 { percent = 100 } - return int((percent / 100.0) * 65535.0) + return int(percent * 100.0) } // NormalizeToPercent normalizes a reading to a percentage based on the provided min and max values. func NormalizeToPercent(reading, min, max float64) float64 { - if max == min { - return 0 // or return NaN/error to avoid division by zero - } - percent := 100 * (reading - min) / (max - min) + // if max == min { + // return 0 // or return NaN/error to avoid division by zero + // } + percent := reading / 100 //* (reading - min) / (max - min) // Clamp to [0, 100] in case reading is outside the expected range if percent < 0 { From 494f96c0a46483210b252fe36f4ad23848e1c9a9 Mon Sep 17 00:00:00 2001 From: vanDeventer Date: Sun, 8 Jun 2025 11:03:48 +0200 Subject: [PATCH 06/81] update a few system with new configuration process --- .gitignore | 2 + ds18b20/ds18b20.go | 19 ++++++++-- ds18b20/thing.go | 73 ++++++++++++++++++----------------- esr/README.md | 16 ++++---- esr/esr.go | 18 ++++++--- esr/thing.go | 74 ++++++++++++++++++++++++++++-------- leveler/thing.go | 26 +++++++------ parallax/parallax.go | 20 +++++++--- parallax/thing.go | 54 ++++++++++++++++++++------ thermostat/README.md | 18 ++++----- thermostat/thermostat.go | 19 ++++++++-- thermostat/thing.go | 82 ++++++++++++++++++++++++++++------------ 12 files changed, 287 insertions(+), 134 deletions(-) diff --git a/.gitignore b/.gitignore index df83fbc..fe2250e 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,5 @@ go.mod go.sum serviceRegistry.db *.pem +**/files/ + diff --git a/ds18b20/ds18b20.go b/ds18b20/ds18b20.go index 2ae678d..12c420a 100644 --- a/ds18b20/ds18b20.go +++ b/ds18b20/ds18b20.go @@ -18,6 +18,7 @@ package main import ( "context" + "crypto/x509/pkix" "encoding/json" "fmt" "log" @@ -43,6 +44,14 @@ func main() { Details: map[string][]string{"Developer": {"Synecdoque"}}, ProtoPort: map[string]int{"https": 0, "http": 20150, "coap": 0}, InfoLink: "https://github.com/sdoque/systems/tree/main/ds18b20", + DName: pkix.Name{ + CommonName: sys.Name, + Organization: []string{"Synecdoque"}, + OrganizationalUnit: []string{"Systems"}, + Locality: []string{"Luleå"}, + Province: []string{"Norrbotten"}, + Country: []string{"SE"}, + }, } // instantiate a template unit asset @@ -51,18 +60,20 @@ func main() { sys.UAssets[assetName] = &assetTemplate // Configure the system - rawResources, servsTemp, err := usecases.Configure(&sys) + rawResources, err := usecases.Configure(&sys) if err != nil { log.Fatalf("configuration error: %v\n", err) } sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) + var cleanups []func() for _, raw := range rawResources { - var uac UnitAsset + var uac usecases.ConfigurableAsset if err := json.Unmarshal(raw, &uac); err != nil { log.Fatalf("resource configuration error: %+v\n", err) } - ua, cleanup := newResource(uac, &sys, servsTemp) - defer cleanup() + ua, cleanup := newResource(uac, &sys) + cleanups = append(cleanups, cleanup) + defer cleanup() // ensure cleanup is called when the program exits sys.UAssets[ua.GetName()] = &ua } diff --git a/ds18b20/thing.go b/ds18b20/thing.go index 348635e..7306b97 100644 --- a/ds18b20/thing.go +++ b/ds18b20/thing.go @@ -18,8 +18,9 @@ package main import ( "context" + "encoding/json" + "fmt" "log" - "math" "os" "strconv" "strings" @@ -27,7 +28,7 @@ import ( "github.com/sdoque/mbaigo/components" "github.com/sdoque/mbaigo/forms" - "golang.org/x/exp/rand" + "github.com/sdoque/mbaigo/usecases" ) // Define the types of requests the measurement manager can handle @@ -37,7 +38,12 @@ type STray struct { Error chan error } -//-------------------------------------Define the unit asset +// -------------------------------------Define the unit asset +// Traits are Asset-specific configurable parameters +type Traits struct { + temperature float64 `json:"-"` + tStamp time.Time `json:"-"` +} // UnitAsset type models the unit asset (interface) of the system. type UnitAsset struct { @@ -47,9 +53,8 @@ type UnitAsset struct { ServicesMap components.Services `json:"-"` CervicesMap components.Cervices `json:"-"` // - temperature float64 `json:"-"` - tStamp time.Time `json:"-"` - trayChan chan STray `json:"-"` // Add a channel for temperature readings + Traits + trayChan chan STray `json:"-"` // Add a channel for temperature readings } // GetName returns the name of the Resource. @@ -72,6 +77,11 @@ func (ua *UnitAsset) GetDetails() map[string][]string { return ua.Details } +// GetTraits returns the traits of the Resource. +func (ua *UnitAsset) GetTraits() any { + return ua.Traits +} + // ensure UnitAsset implements components.UnitAsset (this check is done at during the compilation) var _ components.UnitAsset = (*UnitAsset)(nil) @@ -102,15 +112,21 @@ func initTemplate() components.UnitAsset { //-------------------------------------Instantiate the unit assets based on configuration // newResource creates the Resource resource with its pointers and channels based on the configuration -func newResource(uac UnitAsset, sys *components.System, servs []components.Service) (components.UnitAsset, func()) { +func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.System) (components.UnitAsset, func()) { ua := &UnitAsset{ // this a struct that implements the UnitAsset interface - Name: uac.Name, + Name: configuredAsset.Name, Owner: sys, - Details: uac.Details, - ServicesMap: components.CloneServices(servs), + Details: configuredAsset.Details, + ServicesMap: usecases.MakeServiceMap(configuredAsset.Services), trayChan: make(chan STray), // Initialize the channel } + traits, err := UnmarshalTraits(configuredAsset.Traits) + if err != nil { + log.Println("Warning: could not unmarshal traits:", err) + } else if len(traits) > 0 { + ua.Traits = traits[0] // or handle multiple traits if needed + } // start the unit asset(s) go ua.readTemperature(sys.Ctx) @@ -119,14 +135,25 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi } } +// UnmarshalTraits unmarshals a slice of json.RawMessage into a slice of Traits. +func UnmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { + var traitsList []Traits + for _, raw := range rawTraits { + var t Traits + if err := json.Unmarshal(raw, &t); err != nil { + return nil, fmt.Errorf("failed to unmarshal trait: %w", err) + } + traitsList = append(traitsList, t) + } + return traitsList, nil +} + //-------------------------------------Unit asset's functionalities // readTemperature obtains the temperature from respective ds18b20 resource at regular intervals func (ua *UnitAsset) readTemperature(ctx context.Context) { defer close(ua.trayChan) // Ensure the channel is closed when the goroutine exits - randomdDelay() - // Create a ticker that triggers every 2 seconds ticker := time.NewTicker(2 * time.Second) defer ticker.Stop() // Clean up the ticker when done @@ -198,25 +225,3 @@ func (ua *UnitAsset) readTemperature(ctx context.Context) { } } } - -// randomDelay is used to have the requests to multiple 1-wire sensor out of synch to free the bus. (This is a quick hack :-( ) -func randomdDelay() { - rand.Seed(uint64(time.Now().UnixNano())) - - // Constants - baseDelay := 93 * time.Millisecond // 0.093 seconds - maxMultiples := int(math.Floor(1.0 / 0.093)) // Calculate the max multiples (10 in this case) - - // Generate a random multiplier (1 to maxMultiples - 1) - randomMultiplier := rand.Intn(maxMultiples-1) + 1 - - // Calculate the delay - delay := time.Duration(randomMultiplier) * baseDelay - - log.Printf("Random delay: %v\n", delay) - - // Sleep for the random duration - time.Sleep(delay) - - log.Println("Program resumed after delay.") -} diff --git a/esr/README.md b/esr/README.md index 3e93ba9..47ce25d 100755 --- a/esr/README.md +++ b/esr/README.md @@ -11,23 +11,23 @@ If such tracking is necessary, it is best suited with the Modeler system with it ## Compilation After cloning the *Systems repository*, you will need to go to the *esr* directory in the command line interface or terminal. There, you will need to initialize the *go.mod* file for dependency tracking and version management (this is done only once). -Type ```go mod init esr```. +Type ```go mod init github.com/sdoque/systems/esr```. As it generates the file, it will tell you to tidy it up with ```go mod tidy```. If there are dependencies, (which you can list with ```go list -m all```), it will generate a *go.sum* file with the checksum of the downloaded dependencies for integrity verification. -You can then compile your code with ```go build esr.go thing.go scheduler.go```. +You can then execute the code with ```go run .```. The first time, the program is ran, it will generate the *systemconfig.json*, which you can update if necessary. Then restarting the program, the system will be up and running. It will provide you with the URL of its web server, which you can access with a standard web browser. ## Cross compilation -- Intel Mac: ```GOOS=darwin GOARCH=amd64 go build -o esr_imac esr.go thing.go scheduler.go``` -- ARM Mac: ```GOOS=darwin GOARCH=arm64 go build -o esr_amac esr.go thing.go scheduler.go``` -- Windows 64: ```GOOS=windows GOARCH=amd64 go build -o esr_win64.exe esr.go thing.go scheduler.go``` -- Raspberry Pi 64: ```GOOS=linux GOARCH=arm64 go build -o esr_rpi64 esr.go thing.go scheduler.go``` -- Linux: ```GOOS=linux GOARCH=amd64 go build -o esr_amd64 esr.go thing.go scheduler.go``` +- Intel Mac: ```GOOS=darwin GOARCH=amd64 go build -o esr_imac``` +- ARM Mac: ```GOOS=darwin GOARCH=arm64 go build -o esr_amac``` +- Windows 64: ```GOOS=windows GOARCH=amd64 go build -o esr_win64.exe``` +- Raspberry Pi 64: ```GOOS=linux GOARCH=arm64 go build -o esr_rpi64``` +- Linux: ```GOOS=linux GOARCH=amd64 go build -o esr_amd64``` ## Testing shutdown To test the graceful shutdown, one cannot use the IDE debugger but must use the terminal with -```go run esr.go thing.go scheduler.go``` +```go run .``` Using the IDE debugger will allow one to test device failure, i.e. unplugging the computer. \ No newline at end of file diff --git a/esr/esr.go b/esr/esr.go index 9bd4dce..7b6d655 100644 --- a/esr/esr.go +++ b/esr/esr.go @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2024 Synecdoque + * Copyright (c) 2025 Synecdoque * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -18,6 +18,7 @@ package main import ( "context" + "crypto/x509/pkix" "encoding/json" "fmt" "io" @@ -48,6 +49,14 @@ func main() { Details: map[string][]string{"Developer": {"Synecdoque"}}, ProtoPort: map[string]int{"https": 0, "http": 20102, "coap": 0}, InfoLink: "https://github.com/sdoque/systems/tree/main/esr", + DName: pkix.Name{ + CommonName: sys.Name, + Organization: []string{"Synecdoque"}, + OrganizationalUnit: []string{"Systems"}, + Locality: []string{"Luleå"}, + Province: []string{"Norrbotten"}, + Country: []string{"SE"}, + }, } // instantiate a template unit asset @@ -56,18 +65,17 @@ func main() { sys.UAssets[assetName] = &assetTemplate // Configure the system - rawResources, servsTemp, err := usecases.Configure(&sys) + rawResources, err := usecases.Configure(&sys) if err != nil { log.Fatalf("Configuration error: %v\n", err) } sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) - for _, raw := range rawResources { - var uac UnitAsset + var uac usecases.ConfigurableAsset if err := json.Unmarshal(raw, &uac); err != nil { log.Fatalf("Resource configuration error: %+v\n", err) } - ua, cleanup := newResource(uac, &sys, servsTemp) // a new unit asset with its own mutex + ua, cleanup := newResource(uac, &sys) defer cleanup() sys.UAssets[ua.GetName()] = &ua } diff --git a/esr/thing.go b/esr/thing.go index 2009afc..b26bb69 100644 --- a/esr/thing.go +++ b/esr/thing.go @@ -17,6 +17,7 @@ package main import ( + "encoding/json" "errors" "fmt" "log" @@ -36,10 +37,22 @@ type ServiceRegistryRequest struct { Record forms.Form Id int64 Result chan []forms.ServiceRecord_v1 // For returning records - Error chan error // For error handling + Error chan error } -//-------------------------------------Define the unit asset +// -------------------------------------Define the unit asset +// Traits are Asset-specific configurable parameters and variables +type Traits struct { + serviceRegistry map[int]forms.ServiceRecord_v1 + + recCount int64 + requests chan ServiceRegistryRequest + // Error chan error // For error handling + sched *Scheduler + leading bool + leadingSince time.Time + leadingRegistrar *components.CoreSystem // if not leading this points to the current leader +} // UnitAsset type models the unit asset (interface) of the system type UnitAsset struct { @@ -49,14 +62,8 @@ type UnitAsset struct { ServicesMap components.Services `json:"-"` CervicesMap components.Cervices `json:"-"` // - serviceRegistry map[int]forms.ServiceRecord_v1 - mu sync.Mutex - recCount int64 - requests chan ServiceRegistryRequest - sched *Scheduler - leading bool - leadingSince time.Time - leadingRegistrar *components.CoreSystem // if not leading this points to the current leader + Traits // Embedding the Traits struct to include its fields and methods + mu sync.Mutex } // GetName returns the name of the Resource. @@ -79,6 +86,11 @@ func (ua *UnitAsset) GetDetails() map[string][]string { return ua.Details } +// GetTraits returns the traits of the Resource. +func (ua *UnitAsset) GetTraits() any { + return ua.Traits +} + // ensure UnitAsset implements components.UnitAsset (this check is done at during the compilation) var _ components.UnitAsset = (*UnitAsset)(nil) @@ -115,10 +127,13 @@ func initTemplate() components.UnitAsset { Description: "reports (GET) the role of the Service Registrar as leading or on stand by", } - // var uat components.UnitAsset // this is an interface, which we then initialize + assetTraits := Traits{} + + // Create the UnitAsset with the defined services uat := &UnitAsset{ Name: "registry", Details: map[string][]string{"Location": {"LocalCloud"}}, + Traits: assetTraits, ServicesMap: components.Services{ registerService.SubPath: ®isterService, queryService.SubPath: &queryService, @@ -132,23 +147,36 @@ func initTemplate() components.UnitAsset { //-------------------------------------Instantiate unit asset(s) based on configuration // newResource creates the unit asset with its pointers and channels based on the configuration using the uaConfig structs -func newResource(uac UnitAsset, sys *components.System, servs []components.Service) (components.UnitAsset, func()) { +func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.System) (components.UnitAsset, func()) { // Start the registration expiration check scheduler cleaningScheduler := NewScheduler() go cleaningScheduler.run() // Initialize the UnitAsset ua := &UnitAsset{ - Name: uac.Name, - Owner: sys, - Details: uac.Details, + Name: configuredAsset.Name, + Owner: sys, + Details: configuredAsset.Details, + ServicesMap: usecases.MakeServiceMap(configuredAsset.Services), + } + + traits, err := UnmarshalTraits(configuredAsset.Traits) + if err != nil { + log.Println("Warning: could not unmarshal traits:", err) + } else if len(traits) > 0 { + ua.Traits = traits[0] // or handle multiple traits if needed + } + + assetTraits := Traits{ serviceRegistry: make(map[int]forms.ServiceRecord_v1), recCount: 1, // 0 is used for non registered services sched: cleaningScheduler, - ServicesMap: components.CloneServices(servs), requests: make(chan ServiceRegistryRequest), // Initialize the requests channel + // Error: make(chan error), // Initialize the error channel } + ua.Traits = assetTraits // Assign the traits to the UnitAsset + ua.Role() // Start to repeatedly check which is the leading registrar // Start the service registry manager goroutine @@ -161,6 +189,19 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi } } +// UnmarshalTraits unmarshals a slice of json.RawMessage into a slice of Traits. +func UnmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { + var traitsList []Traits + for _, raw := range rawTraits { + var t Traits + if err := json.Unmarshal(raw, &t); err != nil { + return nil, fmt.Errorf("failed to unmarshal trait: %w", err) + } + traitsList = append(traitsList, t) + } + return traitsList, nil +} + //-------------------------------------Unit's resource methods // There are really two assets here: the database and the scheduler @@ -204,6 +245,7 @@ func (ua *UnitAsset) serviceRegistryHandler() { _, exists := ua.serviceRegistry[rec.Id] if !exists { ua.mu.Unlock() + request.Error <- fmt.Errorf("no existing record with id %d", rec.Id) continue } dbRec := ua.serviceRegistry[rec.Id] diff --git a/leveler/thing.go b/leveler/thing.go index c2243e9..017133e 100644 --- a/leveler/thing.go +++ b/leveler/thing.go @@ -32,15 +32,15 @@ import ( // -------------------------------------Define the unit asset // Traits are Asset-specific configurable parameters and variables type Traits struct { - SetPt float64 `json:"setPoint"` // the set point for the level - Period time.Duration `json:"samplingPeriod"` - Kp float64 `json:"kp"` - Lambda float64 `json:"lambda"` - Ki float64 `json:"ki"` - jitter time.Duration - deviation float64 - integral float64 - previousT float64 // previous level reading to avoid flooding the log + SetPt float64 `json:"setPoint"` // the set point for the level + Period time.Duration `json:"samplingPeriod"` + Kp float64 `json:"kp"` + Lambda float64 `json:"lambda"` + Ki float64 `json:"ki"` + jitter time.Duration + deviation float64 + integral float64 + previousLevel float64 // previous level reading to avoid flooding the log } // UnitAsset type models the unit asset (interface) of the system @@ -115,7 +115,8 @@ func initTemplate() components.UnitAsset { Lambda: 0.5, Ki: 0, } - // var uat components.UnitAsset // this is an interface, which we then initialize + + // create the unit asset template uat := &UnitAsset{ Name: "Leveler_1", Details: map[string][]string{"Location": {"UpperTank"}}, @@ -178,6 +179,7 @@ func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.Sys } } +// UnmarshalTraits unmarshals a slice of json.RawMessage into a slice of Traits. func UnmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { var traitsList []Traits for _, raw := range rawTraits { @@ -282,9 +284,9 @@ func (ua *UnitAsset) processFeedbackLoop() { return } - if tup.Value != ua.previousT { + if tup.Value != ua.previousLevel { log.Printf("the level is %.2f percent with an error %.2f percent and the pumpSpeed set at %.2f%%\n", tup.Value, ua.deviation, output) - ua.previousT = tup.Value + ua.previousLevel = tup.Value } ua.jitter = time.Since(jitterStart) diff --git a/parallax/parallax.go b/parallax/parallax.go index a708b40..edce0bb 100644 --- a/parallax/parallax.go +++ b/parallax/parallax.go @@ -15,6 +15,7 @@ package main import ( "context" + "crypto/x509/pkix" "encoding/json" "fmt" "log" @@ -39,6 +40,14 @@ func main() { Details: map[string][]string{"Developer": {"Arrowhead"}}, ProtoPort: map[string]int{"https": 0, "http": 20151, "coap": 0}, InfoLink: "https://github.com/sdoque/systems/tree/main/parallax", + DName: pkix.Name{ + CommonName: sys.Name, + Organization: []string{"Synecdoque"}, + OrganizationalUnit: []string{"Systems"}, + Locality: []string{"Luleå"}, + Province: []string{"Norrbotten"}, + Country: []string{"SE"}, + }, } // instantiate a template unit asset @@ -47,20 +56,21 @@ func main() { sys.UAssets[assetName] = &assetTemplate // Configure the system - rawResources, servsTemp, err := usecases.Configure(&sys) + rawResources, err := usecases.Configure(&sys) if err != nil { log.Fatalf("Configuration error: %v\n", err) } sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) - // Resources := make(map[string]*UnitAsset) + var cleanups []func() for _, raw := range rawResources { - var uac UnitAsset + var uac usecases.ConfigurableAsset if err := json.Unmarshal(raw, &uac); err != nil { log.Fatalf("Resource configuration error: %+v\n", err) } - ua, cleanup := newResource(uac, &sys, servsTemp) - defer cleanup() + ua, cleanup := newResource(uac, &sys) + cleanups = append(cleanups, cleanup) + defer cleanup() // ensure cleanup is called when the program exits sys.UAssets[ua.GetName()] = &ua } diff --git a/parallax/thing.go b/parallax/thing.go index c702a59..91e1161 100644 --- a/parallax/thing.go +++ b/parallax/thing.go @@ -14,6 +14,7 @@ package main import ( + "encoding/json" "fmt" "log" "time" @@ -24,9 +25,16 @@ import ( "github.com/sdoque/mbaigo/components" "github.com/sdoque/mbaigo/forms" + "github.com/sdoque/mbaigo/usecases" ) -//-------------------------------------Define the unit asset +// -------------------------------------Define the unit asset +// Traits are Asset-specific configurable parameters +type Traits struct { + GpioPin gpio.PinIO `json:"-"` + position int `json:"-"` + dutyChan chan int `json:"-"` +} // UnitAsset type models the unit asset (interface) of the system type UnitAsset struct { @@ -36,9 +44,7 @@ type UnitAsset struct { ServicesMap components.Services `json:"-"` CervicesMap components.Cervices `json:"-"` // - GpioPin gpio.PinIO `json:"-"` - position int `json:"-"` - dutyChan chan int `json:"-"` + Traits } // GetName returns the name of the Resource. @@ -61,6 +67,11 @@ func (ua *UnitAsset) GetDetails() map[string][]string { return ua.Details } +// GetTraits returns the traits of the Resource. +func (ua *UnitAsset) GetTraits() any { + return ua.Traits +} + // ensure UnitAsset implements components.UnitAsset var _ components.UnitAsset = (*UnitAsset)(nil) @@ -80,7 +91,7 @@ func initTemplate() components.UnitAsset { // var uat components.UnitAsset // this is an interface, which we then initialize uat := &UnitAsset{ Name: "Servo_1", - Details: map[string][]string{"Model": {"standard servo", "-90 to +90 degrees"}, "Location": {"Kitchen"}}, + Details: map[string][]string{"Model": {"standard servo", "half_circle"}, "Location": {"Kitchen"}}, ServicesMap: components.Services{ rotation.SubPath: &rotation, // Inline assignment of the rotation service }, @@ -91,16 +102,24 @@ func initTemplate() components.UnitAsset { //-------------------------------------Instantiate the unit assets based on configuration // newResource creates the Resource resource with its pointers and channels based on the configuration using the tConfig structs -func newResource(uac UnitAsset, sys *components.System, servs []components.Service) (components.UnitAsset, func()) { +func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.System) (components.UnitAsset, func()) { // ua components.UnitAsset is an interface, which is implemented and initialized ua := &UnitAsset{ - Name: uac.Name, + Name: configuredAsset.Name, Owner: sys, - Details: uac.Details, - ServicesMap: components.CloneServices(servs), - dutyChan: make(chan int), + Details: configuredAsset.Details, + ServicesMap: usecases.MakeServiceMap(configuredAsset.Services), + } + + traits, err := UnmarshalTraits(configuredAsset.Traits) + if err != nil { + log.Println("Warning: could not unmarshal traits:", err) + } else if len(traits) > 0 { + ua.Traits = traits[0] // or handle multiple traits if needed } + ua.Traits.dutyChan = make(chan int) + // Initialize the periph.io host if _, err := host.Init(); err != nil { log.Fatalf("Failed to initialize periph: %v\n", err) @@ -128,6 +147,19 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi } } +// UnmarshalTraits unmarshals a slice of json.RawMessage into a slice of Traits. +func UnmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { + var traitsList []Traits + for _, raw := range rawTraits { + var t Traits + if err := json.Unmarshal(raw, &t); err != nil { + return nil, fmt.Errorf("failed to unmarshal trait: %w", err) + } + traitsList = append(traitsList, t) + } + return traitsList, nil +} + //-------------------------------------Unit asset's resource functions // timing constants for the PWM (pulse width modulation) @@ -142,7 +174,7 @@ const ( func (ua *UnitAsset) getPosition() (f forms.SignalA_v1a) { f.NewForm() f.Value = float64(ua.position) - f.Unit = "percent" + f.Unit = "Percent" f.Timestamp = time.Now() return f } diff --git a/thermostat/README.md b/thermostat/README.md index 156a1a9..8062ab6 100644 --- a/thermostat/README.md +++ b/thermostat/README.md @@ -10,11 +10,9 @@ It offers three services, *setpoint*, *thermalerror* and *jitter*. The setpoint The control loop is executed every 10 seconds, and can be configured. ## Compiling -To compile the code, one needs to get the AiGo module -```go get github.com/sdoque/mbaigo``` -and initialize the *go.mod* file with ``` go mod init github.com/sdoque/systems/thermostat``` before running *go mod tidy*. +To compile the code, one needs to initialize the *go.mod* file with ``` go mod init github.com/sdoque/systems/thermostat``` before running *go mod tidy*. -To run the code, one just needs to type in ```go run thermostat.go thing.go``` within a terminal or at a command prompt. +To run the code, one just needs to type in ```go run .``` within a terminal or at a command prompt. It is **important** to start the program from within its own directory (and each system should have their own directory) because it looks for its configuration file there. If it does not find it there, it will generate one and shutdown to allow the configuration file to be updated. @@ -26,13 +24,13 @@ To build the software for one's own machine, ## Cross compiling/building The following commands enable one to build for different platforms: -- Intel Mac: ```GOOS=darwin GOARCH=amd64 go build -o thermostat_imac thermostat.go thing.go``` -- ARM Mac: ```GOOS=darwin GOARCH=arm64 go build -o thermostat_amac thermostat.go thing.go``` -- Windows 64: ```GOOS=windows GOARCH=amd64 go build -o thermostat.exe thermostat.go thing.go``` -- Raspberry Pi 64: ```GOOS=linux GOARCH=arm64 go build -o thermostat_rpi64 thermostat.go thing.go``` -- Linux: ```GOOS=linux GOARCH=amd64 go build -o thermostat_linux thermostat.go thing.go``` +- Intel Mac: ```GOOS=darwin GOARCH=amd64 go build -o thermostat_imac ``` +- ARM Mac: ```GOOS=darwin GOARCH=arm64 go build -o thermostat_amac ``` +- Windows 64: ```GOOS=windows GOARCH=amd64 go build -o thermostat.exe``` +- Raspberry Pi 64: ```GOOS=linux GOARCH=arm64 go build -o thermostat_rpi64``` +- Linux: ```GOOS=linux GOARCH=amd64 go build -o thermostat_linux ``` One can find a complete list of platform by typing *‌go tool dist list* at the command prompt If one wants to secure copy it to a Raspberry pi, -`scp thermostat_rpi64 username@ipAddress:mbaigo/thermostat/` where user is the *username* @ the *IP address* of the Raspberry Pi with a relative (to the user's home directory) target *mbaigo/thermostat/* directory. \ No newline at end of file +`scp thermostat_rpi64 username@ipAddress:rpiExec/thermostat/` where user is the *username* @ the *IP address* of the Raspberry Pi with a relative (to the user's home directory) target *rpiExec/thermostat/* directory. \ No newline at end of file diff --git a/thermostat/thermostat.go b/thermostat/thermostat.go index cb7ec83..7f08edf 100644 --- a/thermostat/thermostat.go +++ b/thermostat/thermostat.go @@ -18,6 +18,7 @@ package main import ( "context" + "crypto/x509/pkix" "encoding/json" "fmt" "log" @@ -43,6 +44,14 @@ func main() { Details: map[string][]string{"Developer": {"Synecdoque"}}, ProtoPort: map[string]int{"https": 0, "http": 20152, "coap": 0}, InfoLink: "https://github.com/sdoque/systems/tree/main/thermostat", + DName: pkix.Name{ + CommonName: sys.Name, + Organization: []string{"Synecdoque"}, + OrganizationalUnit: []string{"Systems"}, + Locality: []string{"Luleå"}, + Province: []string{"Norrbotten"}, + Country: []string{"SE"}, + }, } // instantiate a template unit asset @@ -51,17 +60,19 @@ func main() { sys.UAssets[assetName] = &assetTemplate // Configure the system - rawResources, servsTemp, err := usecases.Configure(&sys) + rawResources, err := usecases.Configure(&sys) if err != nil { log.Fatalf("Configuration error: %v\n", err) } sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) + var cleanups []func() for _, raw := range rawResources { - var uac UnitAsset + var uac usecases.ConfigurableAsset if err := json.Unmarshal(raw, &uac); err != nil { - log.Fatalf("Resource configuration error: %+v\n", err) + log.Fatalf("resource configuration error: %+v\n", err) } - ua, cleanup := newResource(uac, &sys, servsTemp) + ua, cleanup := newResource(uac, &sys) + cleanups = append(cleanups, cleanup) defer cleanup() sys.UAssets[ua.GetName()] = &ua } diff --git a/thermostat/thing.go b/thermostat/thing.go index 54c2ac5..ca5ba3f 100644 --- a/thermostat/thing.go +++ b/thermostat/thing.go @@ -18,6 +18,8 @@ package main import ( "context" + "encoding/json" + "fmt" "log" "time" @@ -29,6 +31,18 @@ import ( //-------------------------------------Define the unit asset // UnitAsset type models the unit asset (interface) of the system +// Traits are Asset-specific configurable parameters +type Traits struct { + SetPt float64 `json:"setPoint"` + Period time.Duration `json:"samplingPeriod"` + Kp float64 `json:"kp"` + Lambda float64 `json:"lambda"` + Ki float64 `json:"ki"` + jitter time.Duration `json:"-"` + deviation float64 `json:"-"` + previousT float64 `json:"-"` +} + type UnitAsset struct { Name string `json:"name"` Owner *components.System `json:"-"` @@ -36,14 +50,7 @@ type UnitAsset struct { ServicesMap components.Services `json:"-"` CervicesMap components.Cervices `json:"-"` // - jitter time.Duration - Setpt float64 `json:"setpoint"` - Period time.Duration `json:"samplingPeriod"` - Kp float64 `json:"kp"` - Lambda float64 `json:"lamda"` - Ki float64 `json:"ki"` - deviation float64 - previousT float64 + Traits } // GetName returns the name of the Resource. @@ -66,6 +73,11 @@ func (ua *UnitAsset) GetDetails() map[string][]string { return ua.Details } +// GetTraits returns the traits of the Resource. +func (ua *UnitAsset) GetTraits() any { + return ua.Traits +} + // ensure UnitAsset implements components.UnitAsset (this check is done at during the compilation) var _ components.UnitAsset = (*UnitAsset)(nil) @@ -96,15 +108,19 @@ func initTemplate() components.UnitAsset { Description: "provides the current jitter or control algorithm execution calculated every period (GET)", } + assetTraits := Traits{ + SetPt: 20, + Period: 10, + Kp: 5, + Lambda: 0.5, + Ki: 0, + } + // var uat components.UnitAsset // this is an interface, which we then initialize uat := &UnitAsset{ Name: "controller_1", Details: map[string][]string{"Location": {"Kitchen"}}, - Setpt: 20, - Period: 10, - Kp: 5, - Lambda: 0.5, - Ki: 0, + Traits: assetTraits, ServicesMap: components.Services{ setPointService.SubPath: &setPointService, thermalErrorService.SubPath: &thermalErrorService, @@ -117,7 +133,7 @@ func initTemplate() components.UnitAsset { //-------------------------------------Instantiate the unit assets based on configuration // newResource creates the Resource resource with its pointers and channels based on the configuration using the tConig structs -func newResource(uac UnitAsset, sys *components.System, servs []components.Service) (components.UnitAsset, func()) { +func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.System) (components.UnitAsset, func()) { // determine the protocols that the system supports sProtocols := components.SProtocols(sys.Husk.ProtoPort) // instantiate the consumed services @@ -134,20 +150,23 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi } // instantiate the unit asset ua := &UnitAsset{ - Name: uac.Name, + Name: configuredAsset.Name, Owner: sys, - Details: uac.Details, - ServicesMap: components.CloneServices(servs), - Setpt: uac.Setpt, - Period: uac.Period, - Kp: uac.Kp, - Lambda: uac.Lambda, - Ki: uac.Ki, + Details: configuredAsset.Details, + ServicesMap: usecases.MakeServiceMap(configuredAsset.Services), CervicesMap: components.Cervices{ t.Definition: t, r.Definition: r, }, } + + traits, err := UnmarshalTraits(configuredAsset.Traits) + if err != nil { + log.Println("Warning: could not unmarshal traits:", err) + } else if len(traits) > 0 { + ua.Traits = traits[0] // or handle multiple traits if needed + } + // thermalUnit := ua.ServicesMap["setpoint"].Details["Unit"][0] // the measurement done below are still in Celsius, so allowing it to be configurable does not really make sense at this point ua.CervicesMap["temperature"].Details = components.MergeDetails(ua.Details, map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}) ua.CervicesMap["rotation"].Details = components.MergeDetails(ua.Details, map[string][]string{"Unit": {"Percent"}, "Forms": {"SignalA_v1a"}}) @@ -160,12 +179,25 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi } } +// UnmarshalTraits unmarshals a slice of json.RawMessage into a slice of Traits. +func UnmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { + var traitsList []Traits + for _, raw := range rawTraits { + var t Traits + if err := json.Unmarshal(raw, &t); err != nil { + return nil, fmt.Errorf("failed to unmarshal trait: %w", err) + } + traitsList = append(traitsList, t) + } + return traitsList, nil +} + //-------------------------------------Thing's resource methods // getSetPoint fills out a signal form with the current thermal setpoint func (ua *UnitAsset) getSetPoint() (f forms.SignalA_v1a) { f.NewForm() - f.Value = ua.Setpt + f.Value = ua.SetPt f.Unit = "Celsius" f.Timestamp = time.Now() return f @@ -173,7 +205,7 @@ func (ua *UnitAsset) getSetPoint() (f forms.SignalA_v1a) { // setSetPoint updates the thermal setpoint func (ua *UnitAsset) setSetPoint(f forms.SignalA_v1a) { - ua.Setpt = f.Value + ua.SetPt = f.Value log.Printf("new set point: %.1f", f.Value) } @@ -230,7 +262,7 @@ func (ua *UnitAsset) processFeedbackLoop() { } // perform the control algorithm - ua.deviation = ua.Setpt - tup.Value + ua.deviation = ua.SetPt - tup.Value output := ua.calculateOutput(ua.deviation) // prepare the form to send From 562d2c9d6a8a23d002c679395fb5ae8f8dfbbddd Mon Sep 17 00:00:00 2001 From: vanDeventer Date: Sun, 8 Jun 2025 12:18:24 +0200 Subject: [PATCH 07/81] adding configuration update to Orchestrator --- orchestrator/README.md | 18 ++++++-------- orchestrator/orchestrator.go | 17 ++++++++++--- orchestrator/thing.go | 47 +++++++++++++++++++++++++++++++----- 3 files changed, 62 insertions(+), 20 deletions(-) diff --git a/orchestrator/README.md b/orchestrator/README.md index 6d18e4d..c6a9765 100644 --- a/orchestrator/README.md +++ b/orchestrator/README.md @@ -9,11 +9,9 @@ In the current state, the Orchestrator forwards this request to the Service Regi The Orchestrator has more responsibilities, such as checking the authorization for a system to consume a specific service from another system. These will be implemented in the future. ## Compiling -To compile the code, one needs to get the AiGo module -```go get github.com/sdoque/mbaigo``` -and initialize the *go.mod* file with ``` go mod init github.com/sdoque/systems/orchestrator``` before running *go mod tidy*. +To compile the code, one needs to initialize the *go.mod* file with ``` go mod init github.com/sdoque/systems/orchestrator``` before running *go mod tidy*. -To run the code, one just needs to type in ```go run orchestrator.go thing.go``` within a terminal or at a command prompt. +To run the code, one just needs to type in ```go run .``` within a terminal or at a command prompt. It is **important** to start the program from within its own directory (and each system should have their own directory) because it looks for its configuration file there. If it does not find it there, it will generate one and shutdown to allow the configuration file to be updated. @@ -25,15 +23,15 @@ To build the software for one's own machine, ## Cross compiling/building The following commands enable one to build for different platforms: -- Intel Mac: ```GOOS=darwin GOARCH=amd64 go build -o orchestrator_imac orchestrator.go thing.go``` -- ARM Mac: ```GOOS=darwin GOARCH=arm64 go build -o orchestrator_amac orchestrator.go thing.go``` -- Windows 64: ```GOOS=windows GOARCH=amd64 go build -o orchestrator.exe orchestrator.go thing.go``` -- Raspberry Pi 64: ```GOOS=linux GOARCH=arm64 go build -o orchestrator_rpi64 orchestrator.go thing.go``` -- Linux: ```GOOS=linux GOARCH=amd64 go build -o -o orchestrator_linux orchestrator.go thing.go``` +- Intel Mac: ```GOOS=darwin GOARCH=amd64 go build -o orchestrator_imac``` +- ARM Mac: ```GOOS=darwin GOARCH=arm64 go build -o orchestrator_amac``` +- Windows 64: ```GOOS=windows GOARCH=amd64 go build -o orchestrator.exe``` +- Raspberry Pi 64: ```GOOS=linux GOARCH=arm64 go build -o orchestrator_rpi64``` +- Linux: ```GOOS=linux GOARCH=amd64 go build -o orchestrator_linux``` One can find a complete list of platform by typing *‌go tool dist list* at the command prompt If one wants to secure copy it to a Raspberry pi, -`scp orchestrator_rpi64 username@ipAddress:mbaigo/orchestrator/` where user is the *username* @ the *IP address* of the Raspberry Pi with a relative (to the user's home directory) location *mbaigo/orchestrator/* directory. +`scp orchestrator_rpi64 username@ipAddress:rpiExec/orchestrator/` where user is the *username* @ the *IP address* of the Raspberry Pi with a relative (to the user's home directory) location *rpiExec/orchestrator/* directory. Additionally, one must ensure that the file is an executable file (e.g., ```chmod +x orchestrator_rpi64```). \ No newline at end of file diff --git a/orchestrator/orchestrator.go b/orchestrator/orchestrator.go index 888db94..0da1942 100644 --- a/orchestrator/orchestrator.go +++ b/orchestrator/orchestrator.go @@ -15,6 +15,7 @@ package main import ( "context" + "crypto/x509/pkix" "encoding/json" "fmt" "io" @@ -43,6 +44,14 @@ func main() { Details: map[string][]string{"Developer": {"Arrowhead"}}, ProtoPort: map[string]int{"https": 0, "http": 20103, "coap": 0}, InfoLink: "https://github.com/sdoque/systems/tree/main/orchestrator", + DName: pkix.Name{ + CommonName: sys.Name, + Organization: []string{"Synecdoque"}, + OrganizationalUnit: []string{"Systems"}, + Locality: []string{"Luleå"}, + Province: []string{"Norrbotten"}, + Country: []string{"SE"}, + }, } // instantiate a template unit asset @@ -51,17 +60,17 @@ func main() { sys.UAssets[assetName] = &assetTemplate // Configure the system - rawResources, servsTemp, err := usecases.Configure(&sys) + rawResources, err := usecases.Configure(&sys) if err != nil { log.Fatalf("Configuration error: %v\n", err) } sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) for _, raw := range rawResources { - var uac UnitAsset + var uac usecases.ConfigurableAsset if err := json.Unmarshal(raw, &uac); err != nil { log.Fatalf("Resource configuration error: %+v\n", err) } - ua, cleanup := newResource(uac, &sys, servsTemp) + ua, cleanup := newResource(uac, &sys) defer cleanup() sys.UAssets[ua.GetName()] = &ua } @@ -131,7 +140,7 @@ func (ua *UnitAsset) orchestrate(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - _, err = w.Write(servLocation) // respond with the selected servicelocation + _, err = w.Write(servLocation) // respond with the selected service location if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/orchestrator/thing.go b/orchestrator/thing.go index 6d73f2c..f793fc6 100644 --- a/orchestrator/thing.go +++ b/orchestrator/thing.go @@ -32,6 +32,11 @@ import ( //-------------------------------------Define the Thing's resource +// Traits are Asset-specific configurable parameters and variables +type Traits struct { + leadingRegistrar *components.CoreSystem +} + // UnitAsset type models the unit asset (interface) of the system. type UnitAsset struct { Name string `json:"name"` @@ -40,7 +45,7 @@ type UnitAsset struct { ServicesMap components.Services `json:"-"` CervicesMap components.Cervices `json:"-"` // - leadingRegistrar *components.CoreSystem + Traits } // GetName returns the name of the Resource. @@ -63,6 +68,11 @@ func (ua *UnitAsset) GetDetails() map[string][]string { return ua.Details } +// GetTraits returns the traits of the Resource. +func (ua *UnitAsset) GetTraits() any { + return ua.Traits +} + // ensure UnitAsset implements components.UnitAsset (this check is done at during the compilation) var _ components.UnitAsset = (*UnitAsset)(nil) @@ -78,10 +88,15 @@ func initTemplate() components.UnitAsset { Description: "looks for the desired service described in a quest form (POST)", } - // var uat components.UnitAsset // this is an interface, which we then initialize + assetTraits := Traits{ + leadingRegistrar: nil, // Initialize the leading registrar to nil + } + + // create the unit asset template uat := &UnitAsset{ Name: "orchestration", Details: map[string][]string{"Platform": {"Independent"}}, + Traits: assetTraits, ServicesMap: components.Services{ squest.SubPath: &squest, // Inline assignment of the temperature service }, @@ -92,13 +107,20 @@ func initTemplate() components.UnitAsset { //-------------------------------------Instantiate the unit assets based on configuration // newResource creates the Resource resource with its pointers and channels based on the configuration using the template -func newResource(uac UnitAsset, sys *components.System, servs []components.Service) (components.UnitAsset, func()) { +func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.System) (components.UnitAsset, func()) { // var ua components.UnitAsset // this is an interface, which we then initialize ua := &UnitAsset{ // this is an interface, which we then initialize - Name: uac.Name, + Name: configuredAsset.Name, Owner: sys, - Details: uac.Details, - ServicesMap: components.CloneServices(servs), + Details: configuredAsset.Details, + ServicesMap: usecases.MakeServiceMap(configuredAsset.Services), + } + + traits, err := UnmarshalTraits(configuredAsset.Traits) + if err != nil { + log.Println("Warning: could not unmarshal traits:", err) + } else if len(traits) > 0 { + ua.Traits = traits[0] // or handle multiple traits if needed } // start the unit asset(s) @@ -109,6 +131,19 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi } } +// UnmarshalTraits unmarshals a slice of json.RawMessage into a slice of Traits. +func UnmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { + var traitsList []Traits + for _, raw := range rawTraits { + var t Traits + if err := json.Unmarshal(raw, &t); err != nil { + return nil, fmt.Errorf("failed to unmarshal trait: %w", err) + } + traitsList = append(traitsList, t) + } + return traitsList, nil +} + //-------------------------------------Thing's resource functions // getServiceURL retrieves the service URL for a given ServiceQuest_v1. From 1b9b6e4439bac728bf8e22e58ceca441d00cc8e8 Mon Sep 17 00:00:00 2001 From: vanDeventer Date: Mon, 23 Jun 2025 11:40:01 +0200 Subject: [PATCH 08/81] adding the filmer while addressing new configuration method --- {Influxer => collector}/README.md | 2 +- .../influxer.go => collector/collector.go | 0 {Influxer => collector}/thing.go | 0 filmer/README.md | 29 ++ filmer/filmer.go | 103 ++++++ filmer/thing.go | 203 ++++++++++++ kgrapher/thing.go | 32 +- telegrapher/telegrapher.go | 52 ++- telegrapher/thing.go | 301 ++++++++++++------ 9 files changed, 574 insertions(+), 148 deletions(-) rename {Influxer => collector}/README.md (98%) rename Influxer/influxer.go => collector/collector.go (100%) rename {Influxer => collector}/thing.go (100%) create mode 100644 filmer/README.md create mode 100644 filmer/filmer.go create mode 100644 filmer/thing.go diff --git a/Influxer/README.md b/collector/README.md similarity index 98% rename from Influxer/README.md rename to collector/README.md index 8fc1a37..e93406c 100644 --- a/Influxer/README.md +++ b/collector/README.md @@ -12,7 +12,7 @@ As with the other systems, this is a prototype that shows that the mbaigo librar ## Compiling To compile the code, one needs to get the AiGo module ```go get github.com/vanDeventer/mbaigo``` -and initialize the *go.mod* file with ``` go mod init github.com/vanDeventer/arrowsys/inflxer``` before running *go mod tidy*. +and initialize the *go.mod* file with ``` go mod init github.com/vanDeventer/arrowsys/collector``` before running *go mod tidy*. The reason the *go.mod* file is not included in the repository is that when developing the mbaigo module, a replace statement needs to be included to point to the development code. diff --git a/Influxer/influxer.go b/collector/collector.go similarity index 100% rename from Influxer/influxer.go rename to collector/collector.go diff --git a/Influxer/thing.go b/collector/thing.go similarity index 100% rename from Influxer/thing.go rename to collector/thing.go diff --git a/filmer/README.md b/filmer/README.md new file mode 100644 index 0000000..bb149ad --- /dev/null +++ b/filmer/README.md @@ -0,0 +1,29 @@ +# # mbaigo System: Filmer + +The filmer system takes live streams using connected camera as a service. +The word filmer does not appear in the English dictionary. +The get the stream service stores the file locally and provides a link to get the file. + +## Compiling +To compile the code, one needs to initialize the *go.mod* file with ``` go mod init github.com/sdoque/filmer``` before running *go mod tidy*. + +The reason the *go.mod* file is not included in the repository is that when developing the mbaigo module, a replace statement needs to be included to point to the development code. + +To run the code, one just needs to type in ```go run .``` within a terminal or at a command prompt. + +It is **important** to start the program from within it own directory (and each system should have their own directory) because it looks for its configuration file there. If it does not find it there, it will generate one and shutdown to allow the configuration file to be updated. + +The configuration and operation of the system can be verified using the system's web server using a standard web browser, whose address is provided by the system at startup. + +To build the software for one's own machine, +```go build -o parallax_imac```, where the ending is used to clarify for which platform the code is for. + + +## Cross compiling/building +The following commands enable one to build for different platforms: +- Raspberry Pi 64: ```GOOS=linux GOARCH=arm64 go build -o filmer_rpi64``` + +One can find a complete list of platform by typing *‌go tool dist list* at the command prompt + +If one wants to secure copy it to a Raspberry pi, +`scp filmer_rpi64 jan@192.168.1.195:rpiExec/filmer/` where user is the *username* @ the *IP address* of the Raspberry Pi with a relative (to the user's home directory) target *rpiExec/filmer/* directory.filmer \ No newline at end of file diff --git a/filmer/filmer.go b/filmer/filmer.go new file mode 100644 index 0000000..c2d080c --- /dev/null +++ b/filmer/filmer.go @@ -0,0 +1,103 @@ +/******************************************************************************* + * Copyright (c) 2025 Synecdoque + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, subject to the following conditions: + * + * The software is licensed under the MIT License. See the LICENSE file in this repository for details. + * + * Contributors: + * Jan A. van Deventer, Luleå - initial implementation + * Thomas Hedeler, Hamburg - initial implementation + ***************************************************************************SDG*/ + +package main + +import ( + "context" + "crypto/x509/pkix" + "encoding/json" + "fmt" + "log" + "net/http" + "time" + + "github.com/sdoque/mbaigo/components" + "github.com/sdoque/mbaigo/usecases" +) + +func main() { + // prepare for graceful shutdown + ctx, cancel := context.WithCancel(context.Background()) // create a context that can be cancelled + defer cancel() + + // instantiate the System + sys := components.NewSystem("filmer", ctx) + + // instatiate the husk + sys.Husk = &components.Husk{ + Description: " takes a picture using a camera and saves a file", + Details: map[string][]string{"Developer": {"Arrowhead"}}, + ProtoPort: map[string]int{"https": 0, "http": 20162, "coap": 0}, + InfoLink: "https://github.com/sdoque/mbaigo/tree/master/filmer", + DName: pkix.Name{ + CommonName: sys.Name, + Organization: []string{"Synecdoque"}, + OrganizationalUnit: []string{"Systems"}, + Locality: []string{"Luleå"}, + Province: []string{"Norrbotten"}, + Country: []string{"SE"}, + }, + } + + // instantiate a template unit asset + assetTemplate := initTemplate() + assetName := assetTemplate.GetName() + sys.UAssets[assetName] = &assetTemplate + + // Configure the system + rawResources, err := usecases.Configure(&sys) + if err != nil { + log.Fatalf("Configuration error: %v\n", err) + } + sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) + for _, raw := range rawResources { + var uac usecases.ConfigurableAsset + if err := json.Unmarshal(raw, &uac); err != nil { + log.Fatalf("Resource configuration error: %+v\n", err) + } + ua, cleanup := newResource(uac, &sys) + defer cleanup() + sys.UAssets[ua.GetName()] = &ua + } + + // Generate PKI keys and CSR to obtain a authentication certificate from the CA + usecases.RequestCertificate(&sys) + + // Register the (system) and its services + usecases.RegisterServices(&sys) + + // start the requests handlers and servers + go usecases.SetoutServers(&sys) + + // wait for shutdown signal, and gracefully close properly goroutines with context + <-sys.Sigs // wait for a SIGINT (Ctrl+C) signal + fmt.Println("\nshuting down system", sys.Name) + cancel() // cancel the context, signaling the goroutines to stop + time.Sleep(3 * time.Second) // allow the go routines to be executed, which might take more time than the main routine to end +} + +// Serving handles the resources services. NOTE: it expects those names from the request URL path +func (ua *UnitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath string) { + switch servicePath { + case "start": + fmt.Fprintln(w, ua.StartStreamURL()) + case "stream": + ua.StreamTo(w, r) + default: + http.Error(w, "Invalid service request", http.StatusBadRequest) + } +} diff --git a/filmer/thing.go b/filmer/thing.go new file mode 100644 index 0000000..fe4f18c --- /dev/null +++ b/filmer/thing.go @@ -0,0 +1,203 @@ +/******************************************************************************* + * Copyright (c) 2025 Synecdoque + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, subject to the following conditions: + * + * The software is licensed under the MIT License. See the LICENSE file in this repository for details. + * + * Contributors: + * Jan A. van Deventer, Luleå - initial implementation + * Thomas Hedeler, Hamburg - initial implementation + ***************************************************************************SDG*/ + +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "net/http" + "os/exec" + + "github.com/sdoque/mbaigo/components" + "github.com/sdoque/mbaigo/usecases" +) + +// -------------------------------------Define the unit asset +// Traits are Asset-specific configurable parameters and variables +type Traits struct { +} + +// UnitAsset type models the unit asset (interface) of the system +type UnitAsset struct { + Name string `json:"name"` + Owner *components.System `json:"-"` + Details map[string][]string `json:"details"` + ServicesMap components.Services `json:"-"` + CervicesMap components.Cervices `json:"-"` + Traits +} + +// GetName returns the name of the Resource. +func (ua *UnitAsset) GetName() string { + return ua.Name +} + +// GetServices returns the services of the Resource. +func (ua *UnitAsset) GetServices() components.Services { + return ua.ServicesMap +} + +// GetCervices returns the list of consumed services by the Resource. +func (ua *UnitAsset) GetCervices() components.Cervices { + return ua.CervicesMap +} + +// GetDetails returns the details of the Resource. +func (ua *UnitAsset) GetDetails() map[string][]string { + return ua.Details +} + +// GetTraits returns the traits of the Resource. +func (ua *UnitAsset) GetTraits() any { + return ua.Traits +} + +// ensure UnitAsset implements components.UnitAsset (this check is done at during the compilation) +var _ components.UnitAsset = (*UnitAsset)(nil) + +//-------------------------------------Instantiate a unit asset template + +// initTemplate initializes a UnitAsset with default values. +func initTemplate() components.UnitAsset { + // Define the services that expose the capabilities of the unit asset(s) + stream := components.Service{ + Definition: "stream", + SubPath: "start", + Details: map[string][]string{"Forms": {"mpeg"}}, + Description: " provides a video stream from the camera", + } + + // var uat components.UnitAsset // this is an interface, which we then initialize + uat := &UnitAsset{ + Name: "PiCam", + Details: map[string][]string{"Model": {"PiCam v3 NoIR"}}, + ServicesMap: components.Services{ + stream.SubPath: &stream, // Inline assignment of the temperature service + }, + } + return uat +} + +//-------------------------------------Instantiate the unit assets based on configuration + +// newResource creates the Resource resource with its pointers and channels based on the configuration +func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.System) (components.UnitAsset, func()) { + ua := &UnitAsset{ // this a struct that implements the UnitAsset interface + Name: configuredAsset.Name, + Owner: sys, + Details: configuredAsset.Details, + ServicesMap: usecases.MakeServiceMap(configuredAsset.Services), + } + traits, err := UnmarshalTraits(configuredAsset.Traits) + if err != nil { + log.Println("Warning: could not unmarshal traits:", err) + } else if len(traits) > 0 { + ua.Traits = traits[0] // or handle multiple traits if needed + } + + return ua, func() { + log.Println("disconnecting from sensors") + } +} + +// UnmarshalTraits unmarshals a slice of json.RawMessage into a slice of Traits. +func UnmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { + var traitsList []Traits + for _, raw := range rawTraits { + var t Traits + if err := json.Unmarshal(raw, &t); err != nil { + return nil, fmt.Errorf("failed to unmarshal trait: %w", err) + } + traitsList = append(traitsList, t) + } + return traitsList, nil +} + +//-------------------------------------Unit asset's resource functions + +// StartStreamURL returns the URL to start the video stream. +func (ua *UnitAsset) StartStreamURL() string { + ip := ua.Owner.Host.IPAddresses[0] + port := ua.Owner.Husk.ProtoPort["http"] + return fmt.Sprintf("http://%s:%d/filmer/%s/stream", ip, port, ua.Name) +} + +// StreamTo streams the video from the camera to the HTTP response writer. +// It uses libcamera-vid to capture video and sends it as a multipart MJPEG stream. +func (ua *UnitAsset) StreamTo(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "multipart/x-mixed-replace; boundary=frame") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Access-Control-Allow-Origin", "*") + + cmd := exec.Command("libcamera-vid", + "-t", "0", + "--codec", "mjpeg", + "--width", "640", + "--height", "480", + "--framerate", "15", + "-o", "-") + + stdout, err := cmd.StdoutPipe() + if err != nil { + http.Error(w, "failed to create pipe: "+err.Error(), http.StatusInternalServerError) + return + } + if err := cmd.Start(); err != nil { + http.Error(w, "failed to start libcamera-vid: "+err.Error(), http.StatusInternalServerError) + return + } + go func() { + <-r.Context().Done() + if cmd.Process != nil { + _ = cmd.Process.Kill() + } + }() + + buffer := make([]byte, 0) + temp := make([]byte, 4096) + + for { + n, err := stdout.Read(temp) + if err != nil { + log.Println("stream read error:", err) + break + } + buffer = append(buffer, temp[:n]...) + + for { + start := bytes.Index(buffer, []byte{0xFF, 0xD8}) // JPEG SOI + end := bytes.Index(buffer, []byte{0xFF, 0xD9}) // JPEG EOI + if start >= 0 && end > start { + frame := buffer[start : end+2] + buffer = buffer[end+2:] + + fmt.Fprintf(w, "--frame\r\n") + fmt.Fprintf(w, "Content-Type: image/jpeg\r\n") + fmt.Fprintf(w, "Content-Length: %d\r\n\r\n", len(frame)) + w.Write(frame) + + if f, ok := w.(http.Flusher); ok { + f.Flush() // very important! + } + } else { + break + } + } + } +} diff --git a/kgrapher/thing.go b/kgrapher/thing.go index 9732580..5aceb0f 100644 --- a/kgrapher/thing.go +++ b/kgrapher/thing.go @@ -189,35 +189,15 @@ func resolveLocalOntologies(localOntologies map[string]string, dir string, baseU // assembles ontologies gets the list of systems from the lead registrar and then the ontology of each system func (ua *UnitAsset) assembleOntologies(w http.ResponseWriter) { // Look for leading service registrar - var leadingRegistrar *components.CoreSystem - for _, cSys := range ua.Owner.CoreS { - core := cSys - if core.Name == "serviceregistrar" { - resp, err := http.Get(core.Url + "/status") - if err != nil { - fmt.Println("Error checking service registrar status:", err) - continue - } - bodyBytes, err := io.ReadAll(resp.Body) - resp.Body.Close() - if err != nil { - fmt.Println("Error reading service registrar response body:", err) - continue - } - if strings.HasPrefix(string(bodyBytes), "lead Service Registrar since") { - leadingRegistrar = core - } - } - } - if leadingRegistrar == nil { - fmt.Printf("no service registrar found\n") - http.Error(w, "Internal Server Error: no service registrar found", http.StatusInternalServerError) + leadingRegistrarURL, err := components.GetRunningCoreSystemURL(ua.Owner, "serviceregistrar") + if err != nil { + log.Printf("Error getting the leading service registrar URL: %s\n", err) + http.Error(w, "Internal Server Error: unable to get leading service registrar URL", http.StatusInternalServerError) return } - - // request list of systems in the cloud - leadUrl := leadingRegistrar.Url + "/syslist" + // request list of systems in the cloud from the leading service registrar + leadUrl := leadingRegistrarURL + "/syslist" ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() diff --git a/telegrapher/telegrapher.go b/telegrapher/telegrapher.go index 89a5ba6..aba525d 100644 --- a/telegrapher/telegrapher.go +++ b/telegrapher/telegrapher.go @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2024 Synecdoque + * Copyright (c) 2025 Synecdoque * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -18,6 +18,7 @@ package main import ( "context" + "crypto/x509/pkix" "encoding/json" "fmt" "log" @@ -36,12 +37,20 @@ func main() { // instantiate the System sys := components.NewSystem("telegrapher", ctx) - // instatiate the husk + // instantiate the husk sys.Husk = &components.Husk{ - Description: " subcribes and publishes to an MQTT broker", + Description: " subscribes and publishes to an MQTT broker", Details: map[string][]string{"Developer": {"Synecdoque"}}, ProtoPort: map[string]int{"https": 0, "http": 20172, "coap": 0}, InfoLink: "https://github.com/sdoque/systems/tree/main/telegrapher", + DName: pkix.Name{ + CommonName: sys.Name, + Organization: []string{"Synecdoque"}, + OrganizationalUnit: []string{"Systems"}, + Locality: []string{"Luleå"}, + Province: []string{"Norrbotten"}, + Country: []string{"SE"}, + }, } // instantiate a template unit asset @@ -50,23 +59,19 @@ func main() { sys.UAssets[assetName] = &assetTemplate // Configure the system - rawResources, servsTemp, err := usecases.Configure(&sys) + rawResources, err := usecases.Configure(&sys) if err != nil { log.Fatalf("Configuration error: %v\n", err) } - sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) - // Resources := make(map[string]*UnitAsset) for _, raw := range rawResources { - var uac UnitAsset + var uac usecases.ConfigurableAsset if err := json.Unmarshal(raw, &uac); err != nil { log.Fatalf("Resource configuration error: %+v\n", err) } - promUA, cleanup := newResource(uac, &sys, servsTemp) + ua, cleanup := newResource(uac, &sys) defer cleanup() - for _, nua := range promUA { - sys.UAssets[nua.GetName()] = &nua - } + sys.UAssets[ua.GetName()] = &ua } // Generate PKI keys and CSR to obtain a authentication certificate from the CA @@ -91,15 +96,15 @@ func (ua *UnitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath if svrs[servicePath] != nil { ua.access(w, r, servicePath) } else { - http.Error(w, "Invalid service request [Do not modify the services subpath in the configurration file]", http.StatusBadRequest) + http.Error(w, "Invalid service request [Do not modify the services subpath in the configuration file]", http.StatusBadRequest) } } func (ua *UnitAsset) access(w http.ResponseWriter, r *http.Request, servicePath string) { switch r.Method { case "GET": - msg := messageList[ua.topic] - if msg != nil { + msg := ua.Message + if len(msg) > 0 { w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/json") w.Write(msg) @@ -107,11 +112,24 @@ func (ua *UnitAsset) access(w http.ResponseWriter, r *http.Request, servicePath http.Error(w, "The subscribed topic is not being published", http.StatusBadRequest) } case "PUT": - // sig, err := usecases.HTTPProcessSetRequest(w, r) + // data, err := io.ReadAll(r.Body) // if err != nil { - // log.Println("Error with the setting request of the position ", err) + // http.Error(w, "Failed to read request body", http.StatusBadRequest) + // return // } - // ua.setPosition(sig) + // defer r.Body.Close() + + // if err := ua.publishRaw(data); err != nil { + log.Printf("MQTT client is connected: %v", ua.mClient.IsConnected()) + + if err := ua.publishRaw([]byte(`{"test":123}`)); err != nil { + log.Printf("Failed to publish: %v", err) + http.Error(w, "MQTT publish failed", http.StatusInternalServerError) + return + } + log.Printf("MQTT client is connected: %v", ua.mClient.IsConnected()) + + w.WriteHeader(http.StatusAccepted) default: http.Error(w, "Method is not supported.", http.StatusNotFound) } diff --git a/telegrapher/thing.go b/telegrapher/thing.go index 4b8e9ba..5a2c4ff 100644 --- a/telegrapher/thing.go +++ b/telegrapher/thing.go @@ -17,12 +17,14 @@ package main import ( + "encoding/json" "fmt" "log" "strings" mqtt "github.com/eclipse/paho.mqtt.golang" "github.com/sdoque/mbaigo/components" + "github.com/sdoque/mbaigo/usecases" ) // Define your global variable @@ -33,25 +35,28 @@ func init() { messageList = make(map[string][]byte) } -//-------------------------------------Define the unit asset +// -------------------------------------Define the unit asset +// Traits are Asset-specific configurable parameters and variables +type Traits struct { + Broker string `json:"broker"` + mClient mqtt.Client `json:"-"` + Pattern []string `json:"pattern"` + Username string `json:"username"` + Password string `json:"password"` + Period int `json:"period"` // Period is the time interval for periodic service consumption, e.g., 30 seconds + Topic string `json:"-"` // Topic is the MQTT topic to which the unit asset subscribes + Message []byte `json:"-"` +} // UnitAsset type models the unit asset (interface) of the system type UnitAsset struct { - Name string `json:"name"` + Name string `json:"topic"` Owner *components.System `json:"-"` Details map[string][]string `json:"details"` ServicesMap components.Services `json:"-"` CervicesMap components.Cervices `json:"-"` // - Broker string `json:"broker"` - Topics []string `json:"topics"` - Pattern []string `json:"pattern"` - Username string `json:"username"` - Password string `json:"password"` - client mqtt.Client - topic string - serviceDef string - metatopic string + Traits } // GetName returns the name of the Resource. @@ -74,6 +79,11 @@ func (ua *UnitAsset) GetDetails() map[string][]string { return ua.Details } +// GetTraits returns the traits of the Resource. +func (ua *UnitAsset) GetTraits() any { + return ua.Traits +} + // ensure UnitAsset implements components.UnitAsset (this check is done at during the compilation) var _ components.UnitAsset = (*UnitAsset)(nil) @@ -83,24 +93,28 @@ var _ components.UnitAsset = (*UnitAsset)(nil) func initTemplate() components.UnitAsset { // Define the services that expose the capabilities of the unit asset(s) access := components.Service{ - Definition: "access", + Definition: "temperature", SubPath: "access", Details: map[string][]string{"forms": {"payload"}}, RegPeriod: 30, Description: "Read the current topic message (GET) or publish to it (PUT)", } + assetTraits := Traits{ + Broker: "tcp://localhost:1883", + Username: "user", + Password: "password", + // Topic: "kitchen/temperature", // Default topics + Pattern: []string{"Room"}, // Default patterns e.g. "House", "Room" as in "MyHouse/Kitchen" + } + uat := &UnitAsset{ - Name: "MQTT Broker", + Name: "Kitchen/temperature", Details: map[string][]string{"mqtt": {"home"}}, + Traits: assetTraits, ServicesMap: components.Services{ access.SubPath: &access, }, - Broker: "tcp://localhost:1883", - Username: "user", - Password: "password", - Topics: []string{"kitchen/temperature", "topic2", "topic3"}, // Default topics - Pattern: []string{"pattern1", "pattern2", "pattern3"}, // Default patterns } return uat } @@ -108,115 +122,194 @@ func initTemplate() components.UnitAsset { //-------------------------------------Instantiate the unit assets based on configuration // newResource creates the Resource resource with its pointers and channels based on the configuration using the tConig structs -func newResource(uac UnitAsset, sys *components.System, servs []components.Service) ([]components.UnitAsset, func()) { +func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.System) (components.UnitAsset, func()) { + topic := configuredAsset.Name + assetName := strings.ReplaceAll(topic, "/", "_") + // instantiate the unit asset + ua := &UnitAsset{ + Name: assetName, + Owner: sys, + Details: configuredAsset.Details, + ServicesMap: usecases.MakeServiceMap(configuredAsset.Services), + } + + traits, err := UnmarshalTraits(configuredAsset.Traits) + if err != nil { + log.Println("Warning: could not unmarshal traits:", err) + } else if len(traits) > 0 { + ua.Traits = traits[0] // or handle multiple traits if needed + } + + if len(ua.Pattern) > 0 { + lastSlashIndex := strings.LastIndex(topic, "/") + if lastSlashIndex == -1 { + fmt.Printf("topic %s has no forward slash and is ignored\n", topic) + return nil, func() {} + } + a := topic[:lastSlashIndex] + s := topic[lastSlashIndex+1:] + + // Fill Details from pattern and topic + metaDetails := strings.Split(a, "/") + if ua.Details == nil { + ua.Details = make(map[string][]string) + } + for i := 0; i < len(ua.Pattern) && i < len(metaDetails); i++ { + ua.Details[ua.Pattern[i]] = append(ua.Details[ua.Pattern[i]], metaDetails[i]) + } + + access := components.Service{ + Definition: s, + SubPath: "access", + Details: map[string][]string{"forms": {"mqttPayload"}}, + RegPeriod: 30, + Description: "Read the current topic message (GET) or publish to it (PUT)", + } + ua.ServicesMap[access.SubPath] = &access + + // If the unit asset has a period defined, instantiate the consumed services + // if ua.Period > 0 { + // sProtocols := components.SProtocols(sys.Husk.ProtoPort) + // t := &components.Cervice{ + // Definition: s, + // Protos: sProtocols, + // Nodes: make(map[string][]string), // ✅ Corrected + // } + // if ua.CervicesMap == nil { + // ua.CervicesMap = make(components.Cervices) + // } + // ua.CervicesMap[t.Definition] = t + // } + + } + // Create MQTT client options opts := mqtt.NewClientOptions() - opts.AddBroker(uac.Broker) - opts.SetUsername(uac.Username) - opts.SetPassword(uac.Password) + opts.AddBroker(ua.Broker) + if ua.Username != "" { // Password can be empty string for some brokers + opts.SetUsername(ua.Username) + opts.SetPassword(ua.Password) + } + opts.SetConnectionLostHandler(func(client mqtt.Client, err error) { + log.Printf("Connection lost: %v", err) + }) + opts.SetOnConnectHandler(func(client mqtt.Client) { + log.Println("MQTT connection established") + }) // Create and start the MQTT client - mClient := mqtt.NewClient(opts) - if token := mClient.Connect(); token.Wait() && token.Error() != nil { + log.Println("Connecting to broker:", ua.Traits.Broker) + ua.mClient = mqtt.NewClient(opts) + if token := ua.mClient.Connect(); token.Wait() && token.Error() != nil { log.Fatalf("Error connecting to MQTT broker: %v", token.Error()) } - fmt.Println("Connected to MQTT broker") - assetList := []components.UnitAsset{} - assetMap := make(map[string]components.UnitAsset) // Map asset names to UnitAssets + log.Println("Connected to MQTT broker") // Define the message handler callback messageHandler := func(client mqtt.Client, msg mqtt.Message) { - fmt.Printf("Received A message: %s from topic: %s\n", msg.Payload(), msg.Topic()) + fmt.Printf("Received message: %s from topic: %s\n", msg.Payload(), msg.Topic()) // Ensure the map is initialized (just in case) if messageList == nil { messageList = make(map[string][]byte) } - - messageList[msg.Topic()] = msg.Payload() // Assign message to topic in the map + ua.Message = msg.Payload() // Assign message to topic in the map } - log.Printf("The message list is:\n%+v\n", messageList) + // Subscribe to the topic + if token := ua.mClient.Subscribe(topic, 0, messageHandler); token.Wait() && token.Error() != nil { + log.Fatalf("Error subscribing to topic: %v", token.Error()) + } + fmt.Printf("Subscribed to topic: %s\n", topic) + + // Periodically publish a message to the topic + // if ua.Period > 0 { + // go func(ua *UnitAsset) { + // ticker := time.NewTicker(time.Duration(ua.Period) * time.Second) + // defer ticker.Stop() + // for { + // select { + // case <-ticker.C: + // payload := map[string]interface{}{ + // "value": 0, + // "unit": "celsius", + // "timestamp": time.Now().Format(time.RFC3339), + // "version": "SignalA_v1.0", + // } + // if err := ua.publishToTopic(payload, "application/json"); err != nil { + // log.Printf("Periodic publish failed for topic %s: %v", ua.Topic, err) + // } else { + // log.Printf("Periodic message sent to topic %s", ua.Topic) + // } + // case <-sys.Ctx.Done(): + // log.Printf("Stopping periodic publishing for %s", ua.Topic) + // return + // } + // } + // }(ua) + // } + + return ua, func() { + log.Println("Disconnecting from MQTT broker") + ua.mClient.Disconnect(250) + } +} - for _, topicItem := range uac.Topics { - // Consider the last term of a topic to be a service, and the preceding part is the asset - lastSlashIndex := strings.LastIndex(topicItem, "/") - if lastSlashIndex == -1 { - fmt.Printf("topic %s has no forward slash and is ignored\n", topicItem) - continue +// UnmarshalTraits unmarshals a slice of json.RawMessage into a slice of Traits. +func UnmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { + var traitsList []Traits + for _, raw := range rawTraits { + var t Traits + if err := json.Unmarshal(raw, &t); err != nil { + return nil, fmt.Errorf("failed to unmarshal trait: %w", err) } - a := topicItem[:lastSlashIndex] // The asset part - aName := strings.ReplaceAll(a, "/", "_") - s := topicItem[lastSlashIndex+1:] // The service part + traitsList = append(traitsList, t) + } + return traitsList, nil +} - // Redefine the service - access := components.Service{ - Definition: s, - SubPath: s, - Details: map[string][]string{"forms": {"mqttPayload"}}, - RegPeriod: 30, - Description: "Read the current topic message (GET) or publish to it (PUT)", - } +//-------------------------------------Unit asset's resource functions - // Check if the unit asset already exists in the assetMap - ua, exists := assetMap[aName] - - if !exists { - // Instantiate a new concrete type `MyUnitAsset` implementing `UnitAsset` - ua := &UnitAsset{ - Name: aName, - Owner: sys, - Details: make(map[string][]string), // Initialize the map here - ServicesMap: components.Services{ - access.SubPath: &access, - }, - // Initialize fields - client: mClient, - topic: topicItem, - serviceDef: s, - metatopic: a, - } - - // Add details on the unit asset based on the topic - metaDetails := strings.Split(a, "/") - for i := 0; i < len(uac.Pattern) && i < len(metaDetails); i++ { - ua.Details[uac.Pattern[i]] = append(ua.Details[uac.Pattern[i]], metaDetails[i]) - } - - // Add the new asset to the assetList and assetMap - assetList = append(assetList, ua) - assetMap[aName] = ua - } else { - // If the asset exists, just add the new service to the ServicesMap - ua.(*UnitAsset).ServicesMap[access.SubPath] = &access - } - // Subscribe to the topic - if token := mClient.Subscribe(topicItem, 0, messageHandler); token.Wait() && token.Error() != nil { - log.Fatalf("Error subscribing to topic: %v", token.Error()) - } - fmt.Printf("Subscribed to topic: %s\n", topicItem) +// publishToTopic publishes a payload to the MQTT topic of the unit asset. +func (ua *UnitAsset) publishToTopic(payload map[string]interface{}, contentType string) error { + if ua.mClient == nil { + return fmt.Errorf("MQTT client not initialized") } - return assetList, func() { - log.Println("Disconnecting from MQTT broker") - mClient.Disconnect(250) + + // Serialize the message based on content type + // var data []byte + // var err error + // switch contentType { + // case "application/json": + // data, err = json.Marshal(payload) + // default: + // // Fallback to JSON encoding for now + // data, err = json.Marshal(payload) + // } + // if err != nil { + // return fmt.Errorf("failed to encode payload: %w", err) + // } + log.Println(contentType) + + token := ua.mClient.Publish(ua.Topic, 0, false, payload) + token.Wait() + if token.Error() != nil { + return fmt.Errorf("publish error: %w", token.Error()) } + return nil } -//-------------------------------------Unit asset's resource functions +func (ua *UnitAsset) publishRaw(data []byte) error { + // Just publish and return immediately + token := ua.mClient.Publish(ua.Topic, 0, false, data) -// // subscribeToTopic subscribes to the given MQTT topic -// func (ua *UnitAsset) subscribeToTopic() { -// // Define the message handler callback -// messageHandler := func(client mqtt.Client, msg mqtt.Message) { -// ua.message = string(msg.Payload()) // Convert []byte to string -// fmt.Printf("Received message: %s from topic: %s\n", ua.message, msg.Topic()) -// } - -// // Subscribe to the topic -// theTopic := ua.metatopic + "/" + ua.serviceDef -// if token := ua.client.Subscribe(theTopic, 0, messageHandler); token.Wait() && token.Error() != nil { -// log.Fatalf("Error subscribing to topic: %v", token.Error()) -// } -// fmt.Printf("Subscribed to topic: %s\n", theTopic) -// } + go func() { + token.Wait() + if err := token.Error(); err != nil { + log.Printf("Async publish error: %v", err) + } + }() + + return nil +} From 287b898f895ac41c5d9c15712da9857deb9e93d4 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 9 Jul 2025 16:24:45 +0200 Subject: [PATCH 09/81] Fixes warning about time parsing problem for unregistered service --- esr/thing.go | 1 + 1 file changed, 1 insertion(+) diff --git a/esr/thing.go b/esr/thing.go index b26bb69..332f506 100644 --- a/esr/thing.go +++ b/esr/thing.go @@ -311,6 +311,7 @@ func (ua *UnitAsset) serviceRegistryHandler() { case "delete": // Handle delete record + ua.sched.RemoveTask(int(request.Id)) delete(ua.serviceRegistry, int(request.Id)) if _, exists := ua.serviceRegistry[int(request.Id)]; !exists { log.Printf("The service with ID %d has been deleted.", request.Id) From 7abd0cd16661ba3257d30b3e6a88d1f9e50e1255 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 8 Jul 2025 12:57:56 +0200 Subject: [PATCH 10/81] Adds go.mod files to systems --- .gitignore | 2 -- ds18b20/go.mod | 8 ++++++++ ds18b20/go.sum | 0 esr/go.mod | 8 ++++++++ esr/go.sum | 0 orchestrator/go.mod | 8 ++++++++ orchestrator/go.sum | 0 7 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 ds18b20/go.mod create mode 100644 ds18b20/go.sum create mode 100644 esr/go.mod create mode 100644 esr/go.sum create mode 100644 orchestrator/go.mod create mode 100644 orchestrator/go.sum diff --git a/.gitignore b/.gitignore index fe2250e..9a06519 100644 --- a/.gitignore +++ b/.gitignore @@ -24,8 +24,6 @@ vendor/ # mbaigo *.json -go.mod -go.sum serviceRegistry.db *.pem **/files/ diff --git a/ds18b20/go.mod b/ds18b20/go.mod new file mode 100644 index 0000000..3e5865a --- /dev/null +++ b/ds18b20/go.mod @@ -0,0 +1,8 @@ +module github.com/sdoque/systems/ds18b20 + +go 1.24.4 + +require github.com/sdoque/mbaigo v0.0.0-20250520155324-7390c339652a + +// Replaces this library with a patched version +replace github.com/sdoque/mbaigo v0.0.0-20250520155324-7390c339652a => /home/lmas/code/mbaigo diff --git a/ds18b20/go.sum b/ds18b20/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/esr/go.mod b/esr/go.mod new file mode 100644 index 0000000..189d0ad --- /dev/null +++ b/esr/go.mod @@ -0,0 +1,8 @@ +module github.com/sdoque/systems/esr + +go 1.24.4 + +require github.com/sdoque/mbaigo v0.0.0-20250520155324-7390c339652a + +// Replaces this library with a patched version +replace github.com/sdoque/mbaigo v0.0.0-20250520155324-7390c339652a => /home/lmas/code/mbaigo diff --git a/esr/go.sum b/esr/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/orchestrator/go.mod b/orchestrator/go.mod new file mode 100644 index 0000000..480b6a9 --- /dev/null +++ b/orchestrator/go.mod @@ -0,0 +1,8 @@ +module github.com/sdoque/systems/orchestrator + +go 1.24.4 + +require github.com/sdoque/mbaigo v0.0.0-20250520155324-7390c339652a + +// Replaces this library with a patched version +replace github.com/sdoque/mbaigo v0.0.0-20250520155324-7390c339652a => /home/lmas/code/mbaigo diff --git a/orchestrator/go.sum b/orchestrator/go.sum new file mode 100644 index 0000000..e69de29 From 97f67a0286e1fd39e848b29d7bbb74126d07ec95 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 8 Jul 2025 13:10:14 +0200 Subject: [PATCH 11/81] Adds initial messenger system --- README.md | 3 +- messenger/go.mod | 8 +++++ messenger/go.sum | 0 messenger/messenger.go | 70 ++++++++++++++++++++++++++++++++++++++ messenger/thing.go | 77 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 messenger/go.mod create mode 100644 messenger/go.sum create mode 100644 messenger/messenger.go create mode 100644 messenger/thing.go diff --git a/README.md b/README.md index cc6e617..4d5966d 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Many of the testing is done with the Raspberry Pi (3, 4, &5) with [GPIO](https:/ - 20103 Orchestrator - 20104 Authorizer - 20105 Modeler (local cloud semantics with GraphDB) +- 20106 Messenger - 20150 ds18b20 (1-wire sensor) - 20151 Parallax (PWM) - 20152 Thermostat @@ -37,4 +38,4 @@ Many of the testing is done with the Raspberry Pi (3, 4, &5) with [GPIO](https:/ - 20170 UA client (OPC UA) - 20171 Modboss (Modbus TCP) - 20172 Telegrapher (MQTT) -- 20180 Influxer (Influx DB) \ No newline at end of file +- 20180 Influxer (Influx DB) diff --git a/messenger/go.mod b/messenger/go.mod new file mode 100644 index 0000000..392f772 --- /dev/null +++ b/messenger/go.mod @@ -0,0 +1,8 @@ +module github.com/sdoque/systems/messenger + +go 1.24.4 + +require github.com/sdoque/mbaigo v0.0.0-20250520155324-7390c339652a + +// Replaces this library with a patched version +replace github.com/sdoque/mbaigo v0.0.0-20250520155324-7390c339652a => /home/lmas/code/mbaigo diff --git a/messenger/go.sum b/messenger/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/messenger/messenger.go b/messenger/messenger.go new file mode 100644 index 0000000..7373d0a --- /dev/null +++ b/messenger/messenger.go @@ -0,0 +1,70 @@ +package main + +import ( + "context" + "crypto/x509/pkix" + "encoding/json" + "log" + "net/http" + "time" + + "github.com/sdoque/mbaigo/components" + "github.com/sdoque/mbaigo/usecases" +) + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + sys := components.NewSystem("messenger", ctx) + sys.Husk = &components.Husk{ + Description: "is a logging system that recieves log messages from other systems.", + Details: map[string][]string{"Developer": {"alex"}}, + ProtoPort: map[string]int{"https": 0, "http": 20106, "coap": 0}, + InfoLink: "https://github.com/sdoque/systems/tree/main/messenger", + DName: pkix.Name{ + CommonName: sys.Name, + Organization: []string{"alex"}, + OrganizationalUnit: []string{"Systems"}, + Locality: []string{"Luleå"}, + Province: []string{"Norrbotten"}, + Country: []string{"SE"}, + }, + } + + assetTemplate := initTemplate() + sys.UAssets[assetTemplate.GetName()] = &assetTemplate + rawResources, err := usecases.Configure(&sys) + if err != nil { + log.Fatalf("configuration error: %v\n", err) + } + + sys.UAssets = make(map[string]*components.UnitAsset) + for _, raw := range rawResources { + var uac usecases.ConfigurableAsset + if err := json.Unmarshal(raw, &uac); err != nil { + log.Fatalf("resource configuration error: %+v\n", err) + } + ua, cleanup, err := newResource(uac, &sys) + if err != nil { + log.Fatalf("new resource: %v\n", err) + } + defer cleanup() + sys.UAssets[ua.GetName()] = &ua + } + + usecases.RequestCertificate(&sys) + usecases.RegisterServices(&sys) + go usecases.SetoutServers(&sys) + <-sys.Sigs + log.Println("shuting down", sys.Name) + cancel() + time.Sleep(2 * time.Second) +} + +func (ua *UnitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath string) { + switch servicePath { + default: + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + } +} diff --git a/messenger/thing.go b/messenger/thing.go new file mode 100644 index 0000000..8ad2ac9 --- /dev/null +++ b/messenger/thing.go @@ -0,0 +1,77 @@ +package main + +import ( + "encoding/json" + "fmt" + + "github.com/sdoque/mbaigo/components" + "github.com/sdoque/mbaigo/usecases" +) + +type Traits struct { +} + +type UnitAsset struct { + Name string `json:"name"` + Owner *components.System `json:"-"` + Details map[string][]string `json:"details"` + ServicesMap components.Services `json:"-"` + CervicesMap components.Cervices `json:"-"` + Traits +} + +// TODO: check if pointer is necessary?? +func (ua *UnitAsset) GetName() string { return ua.Name } + +func (ua *UnitAsset) GetServices() components.Services { return ua.ServicesMap } + +func (ua *UnitAsset) GetCervices() components.Cervices { return ua.CervicesMap } + +func (ua *UnitAsset) GetDetails() map[string][]string { return ua.Details } + +func (ua *UnitAsset) GetTraits() any { return ua.Traits } + +var _ components.UnitAsset = &UnitAsset{} + +func initTemplate() components.UnitAsset { + s := components.Service{ + Definition: "message", + SubPath: "message", + Details: map[string][]string{"Forms": {"SystemMessage_v1"}}, + RegPeriod: 30, + Description: "stores a new message in the log database", + } + return &UnitAsset{ + Name: "log", + Details: map[string][]string{}, + ServicesMap: components.Services{s.SubPath: &s}, + } +} + +func newResource(ca usecases.ConfigurableAsset, sys *components.System) (components.UnitAsset, func(), error) { + ua := &UnitAsset{ + Name: ca.Name, + Owner: sys, + Details: ca.Details, + ServicesMap: usecases.MakeServiceMap(ca.Services), + } + traits, err := unmarshalTraits(ca.Traits) + if err != nil { + return nil, nil, err + } + ua.Traits = traits[0] + f := func() {} + return ua, f, nil +} + +func unmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { + var traitsList []Traits + for _, raw := range rawTraits { + var t Traits + if err := json.Unmarshal(raw, &t); err != nil { + return nil, fmt.Errorf("unmarshal trait: %w", err) + } + traitsList = append(traitsList, t) + } + return traitsList, nil +} From a770f7d1df764b5fb4d98d6a09041b2882f1656c Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 9 Jul 2025 16:24:45 +0200 Subject: [PATCH 12/81] Adds basic "beacon" registration of messenger --- messenger/thing.go | 117 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 116 insertions(+), 1 deletion(-) diff --git a/messenger/thing.go b/messenger/thing.go index 8ad2ac9..c4d89b4 100644 --- a/messenger/thing.go +++ b/messenger/thing.go @@ -1,14 +1,25 @@ package main import ( + "bytes" "encoding/json" "fmt" + "io" + "log" + "net/http" + "net/url" + "strconv" + "strings" + "time" "github.com/sdoque/mbaigo/components" + "github.com/sdoque/mbaigo/forms" "github.com/sdoque/mbaigo/usecases" ) type Traits struct { + regMsg []byte // Cached MessengerRegistration form + systems []string // All systems this messenger is registered with } type UnitAsset struct { @@ -31,7 +42,7 @@ func (ua *UnitAsset) GetDetails() map[string][]string { return ua.Details } func (ua *UnitAsset) GetTraits() any { return ua.Traits } -var _ components.UnitAsset = &UnitAsset{} +var _ components.UnitAsset = (*UnitAsset)(nil) func initTemplate() components.UnitAsset { s := components.Service{ @@ -60,6 +71,11 @@ func newResource(ca usecases.ConfigurableAsset, sys *components.System) (compone return nil, nil, err } ua.Traits = traits[0] + ua.regMsg, err = newRegMsg(sys) + if err != nil { + return nil, nil, err + } + go ua.runBeacon() f := func() {} return ua, f, nil } @@ -75,3 +91,102 @@ func unmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { } return traitsList, nil } + +//////////////////////////////////////////////////////////////////////////////// + +// newRegMsg creates a new MessengerRegistration form filled with the system's URL. +// The form is then packed and cached, to be sent periodically by the beacon function. +func newRegMsg(sys *components.System) ([]byte, error) { + // This system URL is created in the same way as the registrar, + // in getUniqueSystems(). Using url.URL instead for safer string assembly. + // https://github.com/lmas/mbaigo_systems/blob/dev/esr/thing.go#L404-L407 + var u url.URL + u.Host = sys.Host.IPAddresses[0] + u.Scheme = "https" + port := sys.Husk.ProtoPort[u.Scheme] + if port == 0 { + u.Scheme = "http" + port = sys.Husk.ProtoPort[u.Scheme] + if port == 0 { + return nil, fmt.Errorf("no http(s) port defined in conf") + } + } + u.Host += ":" + strconv.Itoa(port) + u.Path = sys.Name + m := forms.NewMessengerRegistration_v1(u.String()) + return usecases.Pack(forms.Form(&m), "application/json") +} + +const timeoutUpdate int = 60 + +// runBeacon runs periodically in the background (in a goroutine at startup). +// It fetches a list of systems and then sends out a MessengerRegistration to each. +func (ua *UnitAsset) runBeacon() { + for { + s, err := ua.fetchSystems() + if err != nil { + log.Printf("error fetching system list: %s\n", err) + } + ua.notifySystems(s) + select { + case <-time.Tick(time.Duration(timeoutUpdate) * time.Second): + case <-ua.Owner.Ctx.Done(): + return + } + } +} + +// sendRequest is a helper for sending json web requests. +// It returns either error or the response body as a byte array. +func sendRequest(method, url string, body []byte) ([]byte, error) { + req, err := http.NewRequest(method, url, bytes.NewReader(body)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + if resp.StatusCode < 200 || resp.StatusCode > 299 { + return nil, fmt.Errorf("bad response: %s", resp.Status) + } + defer resp.Body.Close() + return io.ReadAll(resp.Body) +} + +// fetchSystems asks the registrar for a list of online systems. +func (ua *UnitAsset) fetchSystems() (systems []string, err error) { + url, err := components.GetRunningCoreSystemURL( + ua.Owner, components.ServiceRegistrarName) + if err != nil { + return + } + b, err := sendRequest("GET", url+"/syslist", nil) + f, err := usecases.Unpack(b, "application/json") + if err != nil { + return + } + list, ok := f.(*forms.SystemRecordList_v1) + if !ok { + err = fmt.Errorf("form is not a SystemRecordList_v1") + return + } + return list.List, nil +} + +// notifySystems sends a pre-packed MessengerRegistration form to a list of online systems. +// Any systems with incorrect URLs, any messengers, and any http errors will be ignored. +func (ua *UnitAsset) notifySystems(list []string) { + for _, sys := range list { + u, err := url.Parse(sys) + if err != nil { + continue // Skip misconfigured systems + } + if strings.HasPrefix(u.Path, "/"+ua.Owner.Name) { + continue // Skip itself and other messengers + } + // Don't care about any errors or any systems that don't want to talk with us + _, _ = sendRequest("POST", sys+"/msg", ua.regMsg) + } +} From 1dae075c1dc435179b5066da1a48e17f87524675 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 11 Jul 2025 14:58:43 +0200 Subject: [PATCH 13/81] Use the new log functions --- messenger/messenger.go | 9 ++++----- messenger/thing.go | 3 +-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/messenger/messenger.go b/messenger/messenger.go index 7373d0a..4146047 100644 --- a/messenger/messenger.go +++ b/messenger/messenger.go @@ -4,7 +4,6 @@ import ( "context" "crypto/x509/pkix" "encoding/json" - "log" "net/http" "time" @@ -36,18 +35,18 @@ func main() { sys.UAssets[assetTemplate.GetName()] = &assetTemplate rawResources, err := usecases.Configure(&sys) if err != nil { - log.Fatalf("configuration error: %v\n", err) + usecases.LogInfo(&sys, "configuration error: %v", err) } sys.UAssets = make(map[string]*components.UnitAsset) for _, raw := range rawResources { var uac usecases.ConfigurableAsset if err := json.Unmarshal(raw, &uac); err != nil { - log.Fatalf("resource configuration error: %+v\n", err) + usecases.LogError(&sys, "resource configuration error: %+v", err) } ua, cleanup, err := newResource(uac, &sys) if err != nil { - log.Fatalf("new resource: %v\n", err) + usecases.LogError(&sys, "new resource: %v", err) } defer cleanup() sys.UAssets[ua.GetName()] = &ua @@ -57,7 +56,7 @@ func main() { usecases.RegisterServices(&sys) go usecases.SetoutServers(&sys) <-sys.Sigs - log.Println("shuting down", sys.Name) + usecases.LogInfo(&sys, "shuting down %s", sys.Name) cancel() time.Sleep(2 * time.Second) } diff --git a/messenger/thing.go b/messenger/thing.go index c4d89b4..7c58430 100644 --- a/messenger/thing.go +++ b/messenger/thing.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "io" - "log" "net/http" "net/url" "strconv" @@ -125,7 +124,7 @@ func (ua *UnitAsset) runBeacon() { for { s, err := ua.fetchSystems() if err != nil { - log.Printf("error fetching system list: %s\n", err) + usecases.LogWarn(ua.Owner, "error fetching system list: %s", err) } ua.notifySystems(s) select { From 6010d7e9c401cc282136994eaf5688b46c8ae851 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 14 Jul 2025 14:48:58 +0200 Subject: [PATCH 14/81] Set shorter beacon time to minimise the amount of missed msgs --- messenger/thing.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messenger/thing.go b/messenger/thing.go index 7c58430..aa6a8a2 100644 --- a/messenger/thing.go +++ b/messenger/thing.go @@ -116,7 +116,7 @@ func newRegMsg(sys *components.System) ([]byte, error) { return usecases.Pack(forms.Form(&m), "application/json") } -const timeoutUpdate int = 60 +const timeoutUpdate int = 30 // runBeacon runs periodically in the background (in a goroutine at startup). // It fetches a list of systems and then sends out a MessengerRegistration to each. From 8d1d95fa9bd16c8f7dfe133901dda3ec67f74de8 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 14 Jul 2025 14:48:58 +0200 Subject: [PATCH 15/81] Allows the messenger to recieve log msgs from other systems --- messenger/messenger.go | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/messenger/messenger.go b/messenger/messenger.go index 4146047..724a59c 100644 --- a/messenger/messenger.go +++ b/messenger/messenger.go @@ -4,10 +4,13 @@ import ( "context" "crypto/x509/pkix" "encoding/json" + "io" + "log" "net/http" "time" "github.com/sdoque/mbaigo/components" + "github.com/sdoque/mbaigo/forms" "github.com/sdoque/mbaigo/usecases" ) @@ -35,7 +38,8 @@ func main() { sys.UAssets[assetTemplate.GetName()] = &assetTemplate rawResources, err := usecases.Configure(&sys) if err != nil { - usecases.LogInfo(&sys, "configuration error: %v", err) + usecases.LogWarn(&sys, "configuration error: %v", err) + return } sys.UAssets = make(map[string]*components.UnitAsset) @@ -43,6 +47,7 @@ func main() { var uac usecases.ConfigurableAsset if err := json.Unmarshal(raw, &uac); err != nil { usecases.LogError(&sys, "resource configuration error: %+v", err) + return } ua, cleanup, err := newResource(uac, &sys) if err != nil { @@ -63,7 +68,39 @@ func main() { func (ua *UnitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath string) { switch servicePath { + case "message": + ua.handleNewMessage(w, r) default: http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) } } + +func (ua *UnitAsset) handleNewMessage(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + b, err := io.ReadAll(r.Body) + if err != nil { + usecases.LogError(ua.Owner, "read request body: %v", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), + http.StatusInternalServerError) + return + } + defer r.Body.Close() + + f, err := usecases.Unpack(b, r.Header.Get("Content-Type")) + if err != nil { + usecases.LogWarn(ua.Owner, "unpack: %v", err) + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + msg, ok := f.(*forms.SystemMessage_v1) + if !ok { + usecases.LogWarn(ua.Owner, "form is not a SystemMessage_v1") + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + log.Printf("%s: %s\n", msg.System, msg.String()) +} From 2d9e3091a02d0a9039d11d511b34c0b352287da9 Mon Sep 17 00:00:00 2001 From: gabaxh Date: Mon, 14 Jul 2025 16:49:16 +0200 Subject: [PATCH 16/81] Added html files to .gitignore file --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index fe2250e..9d2254d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ *_amac *_ilin *.sh +*.html Makefile # Test binary, built with `go test -c` From 11d62971b76e76b03c1ab700e759277b7a0a4f92 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 14 Jul 2025 14:48:58 +0200 Subject: [PATCH 17/81] TODO: temporary testing logging for ds18 system, COMMIT TO BE REMOVED --- ds18b20/ds18b20.go | 19 +++++++++---------- ds18b20/thing.go | 14 ++++++-------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/ds18b20/ds18b20.go b/ds18b20/ds18b20.go index 12c420a..3f82c00 100644 --- a/ds18b20/ds18b20.go +++ b/ds18b20/ds18b20.go @@ -20,8 +20,6 @@ import ( "context" "crypto/x509/pkix" "encoding/json" - "fmt" - "log" "net/http" "time" @@ -62,14 +60,15 @@ func main() { // Configure the system rawResources, err := usecases.Configure(&sys) if err != nil { - log.Fatalf("configuration error: %v\n", err) + usecases.LogWarn(&sys, "configuration error: %v", err) } sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) var cleanups []func() for _, raw := range rawResources { var uac usecases.ConfigurableAsset if err := json.Unmarshal(raw, &uac); err != nil { - log.Fatalf("resource configuration error: %+v\n", err) + usecases.LogError(&sys, "resource configuration error: %+v", err) + return } ua, cleanup := newResource(uac, &sys) cleanups = append(cleanups, cleanup) @@ -88,7 +87,7 @@ func main() { // wait for shutdown signal, and gracefully close properly goroutines with context <-sys.Sigs // wait for a SIGINT (Ctrl+C) signal - log.Println("\nshuting down system", sys.Name) + usecases.LogInfo(&sys, "shuting down system %s", sys.Name) cancel() // cancel the context, signaling the goroutines to stop time.Sleep(2 * time.Second) // allow the go routines to be executed, which might take more time than the main routine to end } @@ -115,18 +114,18 @@ func (ua *UnitAsset) readTemp(w http.ResponseWriter, r *http.Request) { ua.trayChan <- getMeasuremet select { case err := <-getMeasuremet.Error: - fmt.Printf("Logic error in getting measurement, %s\n", err) - w.WriteHeader(http.StatusInternalServerError) // Use 500 for an internal error + usecases.LogError(ua.Owner, "error getting measurement: %v", err) + w.WriteHeader(http.StatusInternalServerError) return case temperatureForm := <-getMeasuremet.ValueP: usecases.HTTPProcessGetRequest(w, r, &temperatureForm) return case <-time.After(5 * time.Second): // Optional timeout - http.Error(w, "Request timed out", http.StatusGatewayTimeout) - log.Println("Failure to process temperature reading request") + usecases.LogWarn(ua.Owner, "timed out while getting measurement") + http.Error(w, "Measurement timed out", http.StatusInternalServerError) return } default: - http.Error(w, "Method is not supported.", http.StatusNotFound) + http.Error(w, "Method is not supported.", http.StatusMethodNotAllowed) } } diff --git a/ds18b20/thing.go b/ds18b20/thing.go index 7306b97..34befbd 100644 --- a/ds18b20/thing.go +++ b/ds18b20/thing.go @@ -20,7 +20,6 @@ import ( "context" "encoding/json" "fmt" - "log" "os" "strconv" "strings" @@ -123,7 +122,7 @@ func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.Sys traits, err := UnmarshalTraits(configuredAsset.Traits) if err != nil { - log.Println("Warning: could not unmarshal traits:", err) + usecases.LogWarn(sys, "could not unmarshal traits: %v", err) } else if len(traits) > 0 { ua.Traits = traits[0] // or handle multiple traits if needed } @@ -131,7 +130,7 @@ func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.Sys go ua.readTemperature(sys.Ctx) return ua, func() { - log.Printf("disconnecting from %s\n", ua.Name) + usecases.LogInfo(sys, "disconnecting from %s", ua.Name) } } @@ -172,25 +171,25 @@ func (ua *UnitAsset) readTemperature(ctx context.Context) { deviceFile := "/sys/bus/w1/devices/" + ua.Name + "/w1_slave" rawData, err := os.ReadFile(deviceFile) if err != nil { - log.Printf("Error reading temperature file: %s, error: %v\n", deviceFile, err) + usecases.LogError(ua.Owner, "Error reading temperature file: %s, error: %v", deviceFile, err) continue // Retry on the next cycle } if len(rawData) == 0 { - log.Printf("Empty data read from temperature file: %s\n", deviceFile) + usecases.LogWarn(ua.Owner, "Empty data read from temperature file: %s", deviceFile) continue } rawValue := strings.Split(string(rawData), "\n")[1] if !strings.Contains(rawValue, "t=") { - log.Printf("Invalid temperature data: %s\n", rawData) + usecases.LogError(ua.Owner, "Invalid temperature data: %s", rawData) continue } tempStr := strings.Split(rawValue, "t=")[1] temp, err := strconv.ParseFloat(tempStr, 64) if err != nil { - log.Printf("Error parsing temperature: %v\n", err) + usecases.LogError(ua.Owner, "Error parsing temperature: %v", err) continue } @@ -208,7 +207,6 @@ func (ua *UnitAsset) readTemperature(ctx context.Context) { for { select { case <-ctx.Done(): // Shutdown - log.Println("Context canceled, stopping temperature readings.") return case temp := <-tempChan: // Update temperature and timestamp From a438d1a28accc387fa8d15e8695c6ead51f03c11 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 15 Jul 2025 16:05:30 +0200 Subject: [PATCH 18/81] Start tracking log messages and adds simplistic dashboard --- messenger/dashboard.html | 64 ++++++++++++++++++++ messenger/messenger.go | 23 +++++++- messenger/thing.go | 122 +++++++++++++++++++++++++++++++++------ 3 files changed, 188 insertions(+), 21 deletions(-) create mode 100644 messenger/dashboard.html diff --git a/messenger/dashboard.html b/messenger/dashboard.html new file mode 100644 index 0000000..1e438eb --- /dev/null +++ b/messenger/dashboard.html @@ -0,0 +1,64 @@ + + + + + + +Dashboard + + +
+ +

Status

+
    +
  • No hosts.
  • +
+
+ +

Errors

+
    +{{range .Errors}} +
  • {{.}}
  • +{{else}} +
  • No errors.
  • +{{end}} +
+
+ +

Warnings

+
    +{{range .Warnings}} +
  • {{.}}
  • +{{else}} +
  • No warnings.
  • +{{end}} +
+
+ +

Log

+
    +{{range .Logs}} +
  • {{.}}
  • +{{else}} +
  • No logs.
  • +{{end}} +
+
+ +
+ + diff --git a/messenger/messenger.go b/messenger/messenger.go index 724a59c..4f7a47e 100644 --- a/messenger/messenger.go +++ b/messenger/messenger.go @@ -1,11 +1,11 @@ package main import ( + "bytes" "context" "crypto/x509/pkix" "encoding/json" "io" - "log" "net/http" "time" @@ -52,6 +52,7 @@ func main() { ua, cleanup, err := newResource(uac, &sys) if err != nil { usecases.LogError(&sys, "new resource: %v", err) + return } defer cleanup() sys.UAssets[ua.GetName()] = &ua @@ -70,6 +71,8 @@ func (ua *UnitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath switch servicePath { case "message": ua.handleNewMessage(w, r) + case "dashboard": + ua.handleDashboard(w, r) default: http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) } @@ -101,6 +104,22 @@ func (ua *UnitAsset) handleNewMessage(w http.ResponseWriter, r *http.Request) { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } + ua.addMessage(*msg) // Don't want to have to deal with pointers, hence the * +} + +func (ua *UnitAsset) handleDashboard(w http.ResponseWriter, r *http.Request) { + errors, warnings := ua.latestWarnings() + data := map[string]any{ + "Errors": errors, + "Warnings": warnings, + "Logs": ua.latestLogs(), + } - log.Printf("%s: %s\n", msg.System, msg.String()) + buf := &bytes.Buffer{} + if err := ua.tmplDashboard.Execute(buf, data); err != nil { + usecases.LogError(ua.Owner, "execute dashboard: %s", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + buf.WriteTo(w) // Ignoring errors, can't do much with them anyways if the transfer fails } diff --git a/messenger/thing.go b/messenger/thing.go index aa6a8a2..0ebd9bb 100644 --- a/messenger/thing.go +++ b/messenger/thing.go @@ -2,13 +2,16 @@ package main import ( "bytes" - "encoding/json" + _ "embed" "fmt" + "html/template" "io" "net/http" "net/url" + "sort" "strconv" "strings" + "sync" "time" "github.com/sdoque/mbaigo/components" @@ -16,18 +19,40 @@ import ( "github.com/sdoque/mbaigo/usecases" ) -type Traits struct { - regMsg []byte // Cached MessengerRegistration form - systems []string // All systems this messenger is registered with +//go:embed dashboard.html +var tmplDashboard string + +type message struct { + time time.Time + level forms.MessageLevel + system string + body string +} + +func (m message) String() string { + return fmt.Sprintf("%s - %s - %s: %s", + m.system, + m.time.Format("2006-01-02 15:04:05"), + forms.LevelToString(m.level), + m.body, + ) } +// type Traits struct { +// } + type UnitAsset struct { Name string `json:"name"` Owner *components.System `json:"-"` Details map[string][]string `json:"details"` ServicesMap components.Services `json:"-"` CervicesMap components.Cervices `json:"-"` - Traits + // Traits + + regMsg []byte // Cached MessengerRegistration form + messages map[string][]message + mutex sync.RWMutex + tmplDashboard *template.Template } // TODO: check if pointer is necessary?? @@ -39,7 +64,7 @@ func (ua *UnitAsset) GetCervices() components.Cervices { return ua.CervicesMap } func (ua *UnitAsset) GetDetails() map[string][]string { return ua.Details } -func (ua *UnitAsset) GetTraits() any { return ua.Traits } +// func (ua *UnitAsset) GetTraits() any { return ua.Traits } var _ components.UnitAsset = (*UnitAsset)(nil) @@ -64,12 +89,20 @@ func newResource(ca usecases.ConfigurableAsset, sys *components.System) (compone Owner: sys, Details: ca.Details, ServicesMap: usecases.MakeServiceMap(ca.Services), + messages: make(map[string][]message), } - traits, err := unmarshalTraits(ca.Traits) + // traits, err := unmarshalTraits(ca.Traits) + // if err != nil { + // return nil, nil, err + // } + // ua.Traits = traits[0] + + var err error + ua.tmplDashboard, err = template.New("dashboard").Parse(tmplDashboard) if err != nil { return nil, nil, err } - ua.Traits = traits[0] + ua.regMsg, err = newRegMsg(sys) if err != nil { return nil, nil, err @@ -79,17 +112,17 @@ func newResource(ca usecases.ConfigurableAsset, sys *components.System) (compone return ua, f, nil } -func unmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { - var traitsList []Traits - for _, raw := range rawTraits { - var t Traits - if err := json.Unmarshal(raw, &t); err != nil { - return nil, fmt.Errorf("unmarshal trait: %w", err) - } - traitsList = append(traitsList, t) - } - return traitsList, nil -} +// func unmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { +// var traitsList []Traits +// for _, raw := range rawTraits { +// var t Traits +// if err := json.Unmarshal(raw, &t); err != nil { +// return nil, fmt.Errorf("unmarshal trait: %w", err) +// } +// traitsList = append(traitsList, t) +// } +// return traitsList, nil +// } //////////////////////////////////////////////////////////////////////////////// @@ -189,3 +222,54 @@ func (ua *UnitAsset) notifySystems(list []string) { _, _ = sendRequest("POST", sys+"/msg", ua.regMsg) } } + +const maxMessages int = 10 + +func (ua *UnitAsset) addMessage(m forms.SystemMessage_v1) { + ua.mutex.Lock() + defer ua.mutex.Unlock() + + ua.messages[m.System] = append(ua.messages[m.System], message{ + time: time.Now(), + level: m.Level, + system: m.System, + body: m.Body, + }) + if len(ua.messages[m.System]) > maxMessages { + ua.messages[m.System] = ua.messages[m.System][1:] + } +} + +func (ua *UnitAsset) latestWarnings() (errors, warnings map[string]message) { + errors = make(map[string]message) + warnings = make(map[string]message) + ua.mutex.RLock() + defer ua.mutex.RUnlock() + + for system := range ua.messages { + for _, m := range ua.messages[system] { + switch m.level { + case forms.LevelError: + errors[system] = m + case forms.LevelWarn: + warnings[system] = m + } + } + } + return +} + +func (ua *UnitAsset) latestLogs() (logs []message) { + ua.mutex.RLock() + defer ua.mutex.RUnlock() + + for system := range ua.messages { + for _, m := range ua.messages[system] { + logs = append(logs, m) + } + } + sort.Slice(logs, func(i, j int) bool { + return logs[i].time.After(logs[j].time) + }) + return +} From c3789a7b51ee14910e09982ab829f6937fd31428 Mon Sep 17 00:00:00 2001 From: gabaxh Date: Thu, 10 Jul 2025 15:39:17 +0200 Subject: [PATCH 19/81] Started testing the orchestrator system, starting with orchestrate function --- orchestrator/extra_utils_test.go | 189 ++++++++++++++++++++++++++++++ orchestrator/orchestrator_test.go | 152 ++++++++++++++++++++++++ orchestrator/thing.go | 7 +- 3 files changed, 345 insertions(+), 3 deletions(-) create mode 100644 orchestrator/extra_utils_test.go create mode 100644 orchestrator/orchestrator_test.go diff --git a/orchestrator/extra_utils_test.go b/orchestrator/extra_utils_test.go new file mode 100644 index 0000000..f12fa30 --- /dev/null +++ b/orchestrator/extra_utils_test.go @@ -0,0 +1,189 @@ +package main + +import ( + "context" + "encoding/xml" + "fmt" + "net/http" + + "github.com/sdoque/mbaigo/components" + "github.com/sdoque/mbaigo/forms" +) + +// mockTransport is used for replacing the default network Transport (used by +// http.DefaultClient) and it will intercept network requests. +type mockTransport struct { + respFunc func() *http.Response + hits int + err error +} + +func newMockTransport(respFunc func() *http.Response, v int, err error) *mockTransport { + t := &mockTransport{ + respFunc: respFunc, + hits: v, + err: err, + } + // Hijack the default http client so no actual http requests are sent over the network + http.DefaultClient.Transport = t + return t +} + +// RoundTrip method is required to fulfil the RoundTripper interface (as required by the DefaultClient). +// It prevents the request from being sent over the network, and count how many times +// a http request was sent +func (t *mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { + t.hits -= 1 + if t.hits == 0 { + return resp, t.err + } + resp = t.respFunc() + resp.Request = req + return resp, nil +} + +// A mocked UnitAsset used for testing +type mockUnitAsset struct { + Name string `json:"name"` // Must be a unique name, ie. a sensor ID + Owner *components.System `json:"-"` // The parent system this UA is part of + Details map[string][]string `json:"details"` // Metadata or details about this UA + ServicesMap components.Services `json:"-"` + CervicesMap components.Cervices `json:"-"` +} + +func (mua mockUnitAsset) GetName() string { + return mua.Name +} + +func (mua mockUnitAsset) GetServices() components.Services { + return mua.ServicesMap +} + +func (mua mockUnitAsset) GetCervices() components.Cervices { + return mua.CervicesMap +} + +func (mua mockUnitAsset) GetDetails() map[string][]string { + return mua.Details +} + +func (mua mockUnitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath string) {} + +// A mocked form used for testing +type mockForm struct { + XMLName xml.Name `json:"-" xml:"testName"` + Value any `json:"value" xml:"value"` + Unit string `json:"unit" xml:"unit"` + Version string `json:"version" xml:"version"` +} + +// NewForm creates a new form +func (f mockForm) NewForm() forms.Form { + f.Version = "testVersion" + return f +} + +// FormVersion returns the version of the form +func (f mockForm) FormVersion() string { + return f.Version +} + +// Create a error reader to break json.Unmarshal() +type errReader int + +var errBodyRead error = fmt.Errorf("bad body read") + +func (errReader) Read(p []byte) (n int, err error) { + return 0, errBodyRead +} +func (errReader) Close() error { + return nil +} + +// Variables used in testing +var brokenUrl = string(rune(0)) +var errHTTP error = fmt.Errorf("bad http request") + +// Help function to create a test system +func createTestSystem(broken bool) (sys components.System) { + // instantiate the System + ctx := context.Background() + sys = components.NewSystem("testSystem", ctx) + + // Instantiate the Capsule + sys.Husk = &components.Husk{ + Description: "A test system", + Details: map[string][]string{"Developer": {"Test dev"}}, + ProtoPort: map[string]int{"https": 0, "http": 1234, "coap": 0}, + InfoLink: "https://for.testing.purposes", + } + + // create fake services and cervices for a mocked unit asset + testCerv := &components.Cervice{ + Definition: "testCerv", + Details: map[string][]string{"Forms": {"SignalA_v1a"}}, + Nodes: map[string][]string{}, + } + + CervicesMap := &components.Cervices{ + testCerv.Definition: testCerv, + } + setTest := &components.Service{ + ID: 1, + Definition: "squest", + SubPath: "squest", + Details: map[string][]string{"Forms": {"SignalA_v1a"}}, + Description: "A test service", + RegPeriod: 45, + RegTimestamp: "now", + RegExpiration: "45", + } + ServicesMap := &components.Services{ + setTest.SubPath: setTest, + } + assetTraits := Traits{ + leadingRegistrar: nil, + } + mua := &UnitAsset{ + Name: "testUnitAsset", + Details: map[string][]string{"Test": {"Test"}}, + Traits: assetTraits, + ServicesMap: *ServicesMap, + CervicesMap: *CervicesMap, + } + + sys.UAssets = make(map[string]*components.UnitAsset) + var muaInterface components.UnitAsset = mua + sys.UAssets[mua.GetName()] = &muaInterface + + leadingRegistrar := &components.CoreSystem{ + Name: components.ServiceRegistrarName, + Url: "https://leadingregistrar", + } + test := &components.CoreSystem{ + Name: "test", + Url: "https://test", + } + if broken == false { + orchestrator := &components.CoreSystem{ + Name: "orchestrator", + Url: "https://orchestator", + } + sys.CoreS = []*components.CoreSystem{ + leadingRegistrar, + orchestrator, + test, + } + } else { + orchestrator := &components.CoreSystem{ + Name: "orchestrator", + Url: brokenUrl, + } + sys.CoreS = []*components.CoreSystem{ + leadingRegistrar, + orchestrator, + test, + } + } + return +} diff --git a/orchestrator/orchestrator_test.go b/orchestrator/orchestrator_test.go new file mode 100644 index 0000000..1447777 --- /dev/null +++ b/orchestrator/orchestrator_test.go @@ -0,0 +1,152 @@ +package main + +import ( + "bytes" + "encoding/json" + "io" + "log" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/sdoque/mbaigo/components" + "github.com/sdoque/mbaigo/forms" +) + +/* +type servingTestStruct struct { + servicePath string + testName string +} + +var servingTestParams = []servingTestStruct{ + {"squest", "Good case, the service path is squest"}, + {"", "Bad case, the service path is not squest"}, +} + +func TestServing(t *testing.T) { + +} +*/ + +func createUnitAsset() *UnitAsset { + // Define the services that expose the capabilities of the unit asset(s) + squest := components.Service{ + Definition: "squest", + SubPath: "squest", + Details: map[string][]string{"DefaultForm": {"ServiceRecord_v1"}, "Location": {"LocalCloud"}}, + Description: "looks for the desired service described in a quest form (POST)", + } + + assetTraits := Traits{ + leadingRegistrar: &components.CoreSystem{ + Name: components.ServiceRegistrarName, + Url: "https://leadingregistrar", + }, + } + + // create the unit asset template + uat := &UnitAsset{ + Name: "orchestration", + Details: map[string][]string{"Platform": {"Independent"}}, + Traits: assetTraits, + ServicesMap: components.Services{ + squest.SubPath: &squest, // Inline assignment of the temperature service + }, + } + return uat +} + +func createMultiHTTPResponse() func() *http.Response { + count := 0 + return func() *http.Response { + count++ + if count == 2 { + f := createTestServiceRecordListForm() + return &http.Response{ + Status: "200 OK", + StatusCode: 200, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(bytes.NewReader(f)), + } + } + return &http.Response{ + Status: "200 OK", + StatusCode: 200, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(strings.NewReader(string("lead Service Registrar since"))), + } + } +} + +var serviceQuestForm forms.ServiceQuest_v1 + +func createTestServiceQuestForm() []byte { + serviceQuestForm.NewForm() + fakebody, err := json.Marshal(serviceQuestForm) + if err != nil { + log.Fatalf("Fail marshal at start of test: %v", err) + } + return fakebody +} + +var servicePointForm forms.ServicePoint_v1 + +func createTestServicePointForm() []byte { + servicePointForm.NewForm() + servicePointForm.ServLocation = "http://123.456.789:123//" + fakebody, err := json.MarshalIndent(servicePointForm, "", " ") + if err != nil { + log.Fatalf("Fail marshal at start of test: %v", err) + } + return fakebody +} + +var serviceRecordForm forms.ServiceRecord_v1 + +var serviceRecordListForm forms.ServiceRecordList_v1 + +func createTestServiceRecordListForm() []byte { + serviceRecordForm.NewForm() + serviceRecordForm.IPAddresses = []string{"123.456.789"} + serviceRecordForm.ProtoPort = map[string]int{"http": 123} + serviceRecordListForm.NewForm() + serviceRecordListForm.List = []forms.ServiceRecord_v1{serviceRecordForm} + fakebody, err := json.Marshal(serviceRecordListForm) + if err != nil { + log.Fatalf("Fail marshal at start of test: %v", err) + } + return fakebody +} + +type orchestrateTestStruct struct { + inputW http.ResponseWriter + inputBody string + expectedErr bool + expectedOutput string + testName string +} + +var orchestrateTestParams = []orchestrateTestStruct{ + {httptest.NewRecorder(), string(createTestServiceQuestForm()), + false, string(createTestServicePointForm()), "Best case, everything passes"}, +} + +func TestOrchestrate(t *testing.T) { + for _, testCase := range orchestrateTestParams { + inputR := httptest.NewRequest(http.MethodPost, "/test123", io.NopCloser(strings.NewReader(testCase.inputBody))) + inputR.Header.Set("Content-Type", "application/json") + mua := createUnitAsset() + newMockTransport(createMultiHTTPResponse(), 3, nil) + mua.orchestrate(testCase.inputW, inputR) + + recorder, ok := testCase.inputW.(*httptest.ResponseRecorder) + if ok { + if recorder.Body.String() != testCase.expectedOutput { + t.Errorf("In test case: %s: Expected %s, got: %s", + testCase.testName, testCase.expectedOutput, recorder.Body.String()) + } + } + } +} diff --git a/orchestrator/thing.go b/orchestrator/thing.go index f793fc6..9899280 100644 --- a/orchestrator/thing.go +++ b/orchestrator/thing.go @@ -158,7 +158,8 @@ func UnmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { // - servLoc: A byte slice containing the service location in JSON format. // - err: An error if any issues occur during the process. func (ua *UnitAsset) getServiceURL(newQuest forms.ServiceQuest_v1) (servLoc []byte, err error) { - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) // Create a new context, with a 2-second timeout + // ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) // Create a new context, with a 2-second timeout + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second) // Create a new context, with a 2-second timeout defer cancel() sys := ua.Owner if ua.leadingRegistrar != nil { @@ -233,8 +234,8 @@ func (ua *UnitAsset) getServiceURL(newQuest forms.ServiceQuest_v1) (servLoc []by req = req.WithContext(ctx) // associate the cancellable context with the request // forward the request to the leading Service Registrar///////////////////////////////// - client := &http.Client{} - resp, err := client.Do(req) + // client := &http.Client{} + resp, err := http.DefaultClient.Do(req) if err != nil { ua.leadingRegistrar = nil return servLoc, err From 09707edd3e669f95401035afb8deae39a9a54b12 Mon Sep 17 00:00:00 2001 From: gabaxh Date: Fri, 11 Jul 2025 16:58:09 +0200 Subject: [PATCH 20/81] Continued with tests of orchestrator system --- orchestrator/extra_utils_test.go | 82 ++++++++++++++++++- orchestrator/orchestrator_test.go | 122 +++++++++++++++------------- orchestrator/thing.go | 105 +++++++++++++----------- orchestrator/thing_test.go | 130 ++++++++++++++++++++++++++++++ 4 files changed, 331 insertions(+), 108 deletions(-) create mode 100644 orchestrator/thing_test.go diff --git a/orchestrator/extra_utils_test.go b/orchestrator/extra_utils_test.go index f12fa30..fff3ddb 100644 --- a/orchestrator/extra_utils_test.go +++ b/orchestrator/extra_utils_test.go @@ -2,12 +2,10 @@ package main import ( "context" - "encoding/xml" "fmt" "net/http" "github.com/sdoque/mbaigo/components" - "github.com/sdoque/mbaigo/forms" ) // mockTransport is used for replacing the default network Transport (used by @@ -42,6 +40,54 @@ func (t *mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err e return resp, nil } +func createSystemWithUnitAsset(url string) components.System { + ctx := context.Background() + sys := components.NewSystem("testSystem", ctx) + + leadingRegistrar := &components.CoreSystem{ + Name: components.ServiceRegistrarName, + Url: "https://leadingregistrar", + } + sys.CoreS = []*components.CoreSystem{ + leadingRegistrar, + } + return sys +} + +func createUnitAsset(url string) *UnitAsset { + // Define the services that expose the capabilities of the unit asset(s) + squest := components.Service{ + Definition: "squest", + SubPath: "squest", + Details: map[string][]string{"DefaultForm": {"ServiceRecord_v1"}, "Location": {"LocalCloud"}}, + Description: "looks for the desired service described in a quest form (POST)", + } + + assetTraits := Traits{ + leadingRegistrar: &components.CoreSystem{ + Name: components.ServiceRegistrarName, + Url: url, + }, + } + + // create the unit asset template + uat := &UnitAsset{ + Name: "orchestration", + Details: map[string][]string{"Platform": {"Independent"}}, + Traits: assetTraits, + ServicesMap: components.Services{ + squest.SubPath: &squest, // Inline assignment of the temperature service + }, + } + + sys := createSystemWithUnitAsset(url) + uat.Owner = &sys + + return uat +} + +/* + // A mocked UnitAsset used for testing type mockUnitAsset struct { Name string `json:"name"` // Must be a unique name, ie. a sensor ID @@ -88,6 +134,34 @@ func (f mockForm) FormVersion() string { return f.Version } +*/ + +type errorReader struct{} + +func (errorReader) Read(p []byte) (int, error) { + return 0, fmt.Errorf("forced read error") +} + +type mockResponseWriter struct { + http.ResponseWriter +} + +func (e *mockResponseWriter) Write(b []byte) (int, error) { + return 0, fmt.Errorf("Forced write error") +} + +func (e *mockResponseWriter) WriteHeader(statusCode int) {} + +func (e *mockResponseWriter) Header() http.Header { + return make(http.Header) +} + +var brokenUrl = string(rune(0)) + +var errHTTP error = fmt.Errorf("bad http request") + +/* + // Create a error reader to break json.Unmarshal() type errReader int @@ -101,8 +175,6 @@ func (errReader) Close() error { } // Variables used in testing -var brokenUrl = string(rune(0)) -var errHTTP error = fmt.Errorf("bad http request") // Help function to create a test system func createTestSystem(broken bool) (sys components.System) { @@ -187,3 +259,5 @@ func createTestSystem(broken bool) (sys components.System) { } return } + +*/ diff --git a/orchestrator/orchestrator_test.go b/orchestrator/orchestrator_test.go index 1447777..1eb5acc 100644 --- a/orchestrator/orchestrator_test.go +++ b/orchestrator/orchestrator_test.go @@ -1,7 +1,6 @@ package main import ( - "bytes" "encoding/json" "io" "log" @@ -10,65 +9,54 @@ import ( "strings" "testing" - "github.com/sdoque/mbaigo/components" "github.com/sdoque/mbaigo/forms" ) -/* -type servingTestStruct struct { - servicePath string - testName string -} - -var servingTestParams = []servingTestStruct{ - {"squest", "Good case, the service path is squest"}, - {"", "Bad case, the service path is not squest"}, -} - func TestServing(t *testing.T) { - -} -*/ - -func createUnitAsset() *UnitAsset { - // Define the services that expose the capabilities of the unit asset(s) - squest := components.Service{ - Definition: "squest", - SubPath: "squest", - Details: map[string][]string{"DefaultForm": {"ServiceRecord_v1"}, "Location": {"LocalCloud"}}, - Description: "looks for the desired service described in a quest form (POST)", + inputW := httptest.NewRecorder() + inputR := httptest.NewRequest(http.MethodPost, "/test123", io.NopCloser(strings.NewReader(string(createTestServiceQuestForm())))) + inputR.Header.Set("Content-Type", "application/json") + newMockTransport(createMultiHTTPResponse(2, false, string(createTestServiceRecordListForm())), 0, nil) + mua := createUnitAsset("https://leadingregistrar") + mua.Serving(inputW, inputR, "squest") + + var expectedOutput = string(createTestServicePointForm()) + + if inputW.Body.String() != expectedOutput || inputW.Code != 200 { + t.Errorf("Expected %s and code %d, got: %s and code %d", + expectedOutput, 200, inputW.Body.String(), inputW.Code) } - assetTraits := Traits{ - leadingRegistrar: &components.CoreSystem{ - Name: components.ServiceRegistrarName, - Url: "https://leadingregistrar", - }, - } + inputW = httptest.NewRecorder() + inputR = httptest.NewRequest(http.MethodPost, "/test123", io.NopCloser(strings.NewReader(""))) + inputR.Header.Set("Content-Type", "application/json") + newMockTransport(createMultiHTTPResponse(2, false, string(createTestServiceRecordListForm())), 0, nil) + mua = createUnitAsset("https://leadingregistrar") + mua.Serving(inputW, inputR, "wrong") - // create the unit asset template - uat := &UnitAsset{ - Name: "orchestration", - Details: map[string][]string{"Platform": {"Independent"}}, - Traits: assetTraits, - ServicesMap: components.Services{ - squest.SubPath: &squest, // Inline assignment of the temperature service - }, + if inputW.Code == 200 { + t.Errorf("Expected the error code to not be 200 when having servicePath not be squest") } - return uat } -func createMultiHTTPResponse() func() *http.Response { +func createMultiHTTPResponse(limit int, writeError bool, body string) func() *http.Response { count := 0 return func() *http.Response { count++ - if count == 2 { - f := createTestServiceRecordListForm() + if count == limit && writeError == true { return &http.Response{ Status: "200 OK", StatusCode: 200, Header: http.Header{"Content-Type": []string{"application/json"}}, - Body: io.NopCloser(bytes.NewReader(f)), + Body: io.NopCloser(errorReader{}), + } + } + if count == limit { + return &http.Response{ + Status: "200 OK", + StatusCode: 200, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(strings.NewReader(body)), } } return &http.Response{ @@ -120,33 +108,57 @@ func createTestServiceRecordListForm() []byte { return fakebody } +var getServiceURLErrorMessage = "core system 'serviceregistrar' not found: verifying core URL: Get " + + "\"https://leadingregistrar/status\": http: RoundTripper implementation (*main.mockTransport) returned a nil " + + "*Response with a nil error\n" + type orchestrateTestStruct struct { - inputW http.ResponseWriter - inputBody string - expectedErr bool - expectedOutput string - testName string + inputW http.ResponseWriter + inputBody io.ReadCloser + httpMethod string + contentType string + mockTransportErr int + expectedCode int + expectedOutput string + testName string } var orchestrateTestParams = []orchestrateTestStruct{ - {httptest.NewRecorder(), string(createTestServiceQuestForm()), - false, string(createTestServicePointForm()), "Best case, everything passes"}, + {httptest.NewRecorder(), io.NopCloser(strings.NewReader(string(createTestServiceQuestForm()))), "POST", + "application/json", 3, 200, string(createTestServicePointForm()), "Best case, everything passes"}, + {httptest.NewRecorder(), io.NopCloser(strings.NewReader(string(createTestServiceQuestForm()))), "POST", + "", 3, 200, "", "Bad case, header content type is wrong"}, + {httptest.NewRecorder(), io.NopCloser(errorReader{}), "POST", + "application/json", 3, 200, "", "Bad case, ReadAll on header body fails"}, + {httptest.NewRecorder(), io.NopCloser(strings.NewReader(string("hej hej"))), "POST", + "text/plain", 3, 200, "", "Bad case, Unpack and type assertion to ServiceQuest_v1 fails"}, + {httptest.NewRecorder(), io.NopCloser(strings.NewReader(string(createTestServiceQuestForm()))), "POST", + "application/json", 1, 503, getServiceURLErrorMessage, "Bad case, getServiceURL fails"}, + {&mockResponseWriter{}, io.NopCloser(strings.NewReader(string(createTestServiceQuestForm()))), "POST", + "application/json", 3, 0, "", "Bad case, write fails"}, + {httptest.NewRecorder(), io.NopCloser(strings.NewReader(string(""))), "PUT", + "", 0, 404, "Method is not supported.\n", "Bad case, wrong http method"}, } func TestOrchestrate(t *testing.T) { for _, testCase := range orchestrateTestParams { - inputR := httptest.NewRequest(http.MethodPost, "/test123", io.NopCloser(strings.NewReader(testCase.inputBody))) - inputR.Header.Set("Content-Type", "application/json") - mua := createUnitAsset() - newMockTransport(createMultiHTTPResponse(), 3, nil) + inputR := httptest.NewRequest(testCase.httpMethod, "/test123", testCase.inputBody) + inputR.Header.Set("Content-Type", testCase.contentType) + mua := createUnitAsset("https://leadingregistrar") + newMockTransport(createMultiHTTPResponse(2, false, string(createTestServiceRecordListForm())), + testCase.mockTransportErr, nil) mua.orchestrate(testCase.inputW, inputR) recorder, ok := testCase.inputW.(*httptest.ResponseRecorder) if ok { - if recorder.Body.String() != testCase.expectedOutput { + if recorder.Body.String() != testCase.expectedOutput || recorder.Code != testCase.expectedCode { t.Errorf("In test case: %s: Expected %s, got: %s", testCase.testName, testCase.expectedOutput, recorder.Body.String()) } + } else { + if _, ok := testCase.inputW.(*mockResponseWriter); !ok { + t.Errorf("Expected inputW to be of type mockResponseWriter") + } } } } diff --git a/orchestrator/thing.go b/orchestrator/thing.go index 9899280..220a3fe 100644 --- a/orchestrator/thing.go +++ b/orchestrator/thing.go @@ -22,7 +22,8 @@ import ( "log" "net/http" "strconv" - "strings" + + //"strings" "time" "github.com/sdoque/mbaigo/components" @@ -158,61 +159,67 @@ func UnmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { // - servLoc: A byte slice containing the service location in JSON format. // - err: An error if any issues occur during the process. func (ua *UnitAsset) getServiceURL(newQuest forms.ServiceQuest_v1) (servLoc []byte, err error) { - // ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) // Create a new context, with a 2-second timeout - ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second) // Create a new context, with a 2-second timeout + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) // Create a new context, with a 2-second timeout + // ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second) // Create a new context, with a 2-second timeout defer cancel() sys := ua.Owner - if ua.leadingRegistrar != nil { - - // verify that this leading registrar is still leading - resp, errs := http.Get(ua.leadingRegistrar.Url + "/status") - if errs != nil { - log.Println("lost leading registrar status:", errs) - ua.leadingRegistrar = nil - err = errs - return // Skip to the next iteration of the loop - } - - // Read from status resp.Body and then close it directly after - bodyBytes, errs := io.ReadAll(resp.Body) - resp.Body.Close() // Close the body directly after reading from it - if errs != nil { - log.Println("\rError reading response from leading registrar:", errs) - ua.leadingRegistrar = nil - err = errs - return // Skip to the next iteration of the loop - } - - // reset the pointer if the registrar lost its leading status - if !strings.HasPrefix(string(bodyBytes), "lead Service Registrar since") { - ua.leadingRegistrar = nil - log.Println("lost previous leading registrar") - } - } else { - for _, cSys := range sys.CoreS { - core := cSys - if core.Name == "serviceregistrar" { - resp, err := http.Get(core.Url + "/status") - if err != nil { - fmt.Println("Error checking service registrar status:", err) - ua.leadingRegistrar = nil // clear the leading registrar record - continue // Skip to the next iteration of the loop - } + /* + if ua.leadingRegistrar != nil { + + // verify that this leading registrar is still leading + resp, errs := http.Get(ua.leadingRegistrar.Url + "/status") + if errs != nil { + log.Println("lost leading registrar status:", errs) + ua.leadingRegistrar = nil + err = errs + return // Skip to the next iteration of the loop + } - // Read from resp.Body and then close it directly after - bodyBytes, err := io.ReadAll(resp.Body) - resp.Body.Close() // Close the body directly after reading from it - if err != nil { - fmt.Println("Error reading service registrar response body:", err) - continue // Skip to the next iteration of the loop - } + // Read from status resp.Body and then close it directly after + bodyBytes, errs := io.ReadAll(resp.Body) + defer resp.Body.Close() // Close the body directly after reading from it + if errs != nil { + log.Println("\rError reading response from leading registrar:", errs) + ua.leadingRegistrar = nil + err = errs + return // Skip to the next iteration of the loop + } - if strings.HasPrefix(string(bodyBytes), "lead Service Registrar since") { - ua.leadingRegistrar = core - fmt.Printf("\nlead registrar found at: %s\n", ua.leadingRegistrar.Url) + // reset the pointer if the registrar lost its leading status + if !strings.HasPrefix(string(bodyBytes), "lead Service Registrar since") { + ua.leadingRegistrar = nil + log.Println("lost previous leading registrar") + } + } else { + for _, cSys := range sys.CoreS { + core := cSys + if core.Name == "serviceregistrar" { + resp, err := http.Get(core.Url + "/status") + if err != nil { + fmt.Println("Error checking service registrar status:", err) + ua.leadingRegistrar = nil // clear the leading registrar record + continue // Skip to the next iteration of the loop + } + + // Read from resp.Body and then close it directly after + bodyBytes, err := io.ReadAll(resp.Body) + defer resp.Body.Close() // Close the body directly after reading from it + if err != nil { + fmt.Println("Error reading service registrar response body:", err) + continue // Skip to the next iteration of the loop + } + + if strings.HasPrefix(string(bodyBytes), "lead Service Registrar since") { + ua.leadingRegistrar = core + fmt.Printf("\nlead registrar found at: %s\n", ua.leadingRegistrar.Url) + } } } } + */ + ua.leadingRegistrar.Url, err = components.GetRunningCoreSystemURL(sys, ua.leadingRegistrar.Name) + if err != nil { + return servLoc, err } // Create a new HTTP request to the the Service Registrar diff --git a/orchestrator/thing_test.go b/orchestrator/thing_test.go new file mode 100644 index 0000000..6f369d8 --- /dev/null +++ b/orchestrator/thing_test.go @@ -0,0 +1,130 @@ +package main + +import ( + "bytes" + "encoding/json" + "io" + "log" + "net/http" + "strings" + "testing" + + "github.com/sdoque/mbaigo/forms" + "github.com/sdoque/mbaigo/usecases" +) + +func createTestServiceQuest() forms.ServiceQuest_v1 { + var ServiceQuest_v1 forms.ServiceQuest_v1 + ServiceQuest_v1.NewForm() + return ServiceQuest_v1 +} + +func (ua *UnitAsset) createDelayedBrokenURL(limit int) func() *http.Response { + count := 0 + return func() *http.Response { + count++ + if count == limit { + f := createTestServiceRecordListForm() + ua.leadingRegistrar.Url = brokenUrl + return &http.Response{ + Status: "200 OK", + StatusCode: 200, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(bytes.NewReader(f)), + } + } + return &http.Response{ + Status: "200 OK", + StatusCode: 200, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(strings.NewReader(string("lead Service Registrar since"))), + } + } +} + +var emptyServiceRecordListForm forms.ServiceRecordList_v1 + +func createEmptyServiceRecordListForm() []byte { + emptyServiceRecordListForm.NewForm() + fakebody, err := json.Marshal(emptyServiceRecordListForm) + if err != nil { + log.Fatalf("Fail marshal at start of test: %v", err) + } + return fakebody +} + +type getServiceURLTestStruct struct { + inputForm forms.ServiceQuest_v1 + inputBody string + leadingRegistrarUrl string + brokenUrl bool + writeError bool + mockTransportErr int + errHTTP error + expectedOutput string + expectedErr bool + testName string +} + +var getServiceURLTestParams = []getServiceURLTestStruct{ + {createTestServiceQuest(), string(createTestServiceRecordListForm()), "https://leadingregistrar", false, false, + 0, nil, string(createTestServicePointForm()), false, "Good case, everything passes"}, + // {createTestServiceQuest(), string(createTestServiceRecordListForm()), "https://leadingregistrar", true, false, + // 0, nil, "", true, "Bad case, creating a new http request fails"}, + {createTestServiceQuest(), string(createTestServiceRecordListForm()), "https://leadingregistrar", false, false, + 2, errHTTP, "", true, "Bad case, DefaultClient.Do fails"}, + {createTestServiceQuest(), string(createTestServiceRecordListForm()), "https://leadingregistrar", false, true, + 0, nil, "", true, "Bad case, ReadAll fails"}, + {createTestServiceQuest(), "hej hej", "https://leadingregistrar", false, false, + 0, nil, "", true, "Bad case, Unpack fails"}, + {createTestServiceQuest(), string(createTestServicePointForm()), "https://leadingregistrar", false, false, + 0, nil, "", false, "Bad case, type assertion fails"}, + {createTestServiceQuest(), string(createEmptyServiceRecordListForm()), "https://leadingregistrar", false, false, + 0, nil, "", true, "Bad case, the service record list is empty"}, +} + +func TestGetServiceURL(t *testing.T) { + for _, testCase := range getServiceURLTestParams { + mua := createUnitAsset(testCase.leadingRegistrarUrl) + if mua == nil { + t.Fatalf("UAssets[\"Orchestration\"] is nil") + } + if testCase.brokenUrl == true { + newMockTransport(mua.createDelayedBrokenURL(2), testCase.mockTransportErr, testCase.errHTTP) + } else { + newMockTransport(createMultiHTTPResponse(2, testCase.writeError, testCase.inputBody), + testCase.mockTransportErr, testCase.errHTTP) + } + servLoc, err := mua.getServiceURL(testCase.inputForm) + if string(servLoc) != testCase.expectedOutput || (err == nil && testCase.expectedErr == true) || + (err != nil && testCase.expectedErr == false) { + t.Errorf("In test case: %s: Expected %s and error %t, got: %s and %v", + testCase.testName, testCase.expectedOutput, testCase.expectedErr, string(servLoc), err) + } + } +} + +func TestSelectService(t *testing.T) { + serviceListbytes := createTestServiceRecordListForm() + serviceListf, err := usecases.Unpack(serviceListbytes, "application/json") + if err != nil { + log.Fatalf("Error setting up test of SelectService function: %v", err) + } + serviceList, ok := serviceListf.(*forms.ServiceRecordList_v1) + if !ok { + log.Fatalf("Error in type assertion when setting up test of SelectService function") + } + + expectedService := createTestServicePointForm() + + receivedServicef := selectService(*serviceList) + + receivedService, err := usecases.Pack(&receivedServicef, "application/json") + if err != nil { + t.Errorf("Expected the received service to be of type forms.ServicePoint_v1, got: %v", receivedService) + } + + if string(expectedService) != string(receivedService) { + t.Errorf("Expected: %v, got: %v", expectedService, receivedService) + } +} From f0edfc6a0c956f2a9e9ba6e632689842b87f1efb Mon Sep 17 00:00:00 2001 From: gabaxh Date: Mon, 14 Jul 2025 12:16:51 +0200 Subject: [PATCH 21/81] Cleaned up some code --- orchestrator/thing.go | 55 -------------------------------------- orchestrator/thing_test.go | 18 +++++++++++++ 2 files changed, 18 insertions(+), 55 deletions(-) diff --git a/orchestrator/thing.go b/orchestrator/thing.go index 220a3fe..7fe6790 100644 --- a/orchestrator/thing.go +++ b/orchestrator/thing.go @@ -23,7 +23,6 @@ import ( "net/http" "strconv" - //"strings" "time" "github.com/sdoque/mbaigo/components" @@ -163,60 +162,6 @@ func (ua *UnitAsset) getServiceURL(newQuest forms.ServiceQuest_v1) (servLoc []by // ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second) // Create a new context, with a 2-second timeout defer cancel() sys := ua.Owner - /* - if ua.leadingRegistrar != nil { - - // verify that this leading registrar is still leading - resp, errs := http.Get(ua.leadingRegistrar.Url + "/status") - if errs != nil { - log.Println("lost leading registrar status:", errs) - ua.leadingRegistrar = nil - err = errs - return // Skip to the next iteration of the loop - } - - // Read from status resp.Body and then close it directly after - bodyBytes, errs := io.ReadAll(resp.Body) - defer resp.Body.Close() // Close the body directly after reading from it - if errs != nil { - log.Println("\rError reading response from leading registrar:", errs) - ua.leadingRegistrar = nil - err = errs - return // Skip to the next iteration of the loop - } - - // reset the pointer if the registrar lost its leading status - if !strings.HasPrefix(string(bodyBytes), "lead Service Registrar since") { - ua.leadingRegistrar = nil - log.Println("lost previous leading registrar") - } - } else { - for _, cSys := range sys.CoreS { - core := cSys - if core.Name == "serviceregistrar" { - resp, err := http.Get(core.Url + "/status") - if err != nil { - fmt.Println("Error checking service registrar status:", err) - ua.leadingRegistrar = nil // clear the leading registrar record - continue // Skip to the next iteration of the loop - } - - // Read from resp.Body and then close it directly after - bodyBytes, err := io.ReadAll(resp.Body) - defer resp.Body.Close() // Close the body directly after reading from it - if err != nil { - fmt.Println("Error reading service registrar response body:", err) - continue // Skip to the next iteration of the loop - } - - if strings.HasPrefix(string(bodyBytes), "lead Service Registrar since") { - ua.leadingRegistrar = core - fmt.Printf("\nlead registrar found at: %s\n", ua.leadingRegistrar.Url) - } - } - } - } - */ ua.leadingRegistrar.Url, err = components.GetRunningCoreSystemURL(sys, ua.leadingRegistrar.Name) if err != nil { return servLoc, err diff --git a/orchestrator/thing_test.go b/orchestrator/thing_test.go index 6f369d8..7078f58 100644 --- a/orchestrator/thing_test.go +++ b/orchestrator/thing_test.go @@ -13,6 +13,24 @@ import ( "github.com/sdoque/mbaigo/usecases" ) +func TestInitTemplate(t *testing.T) { + expectedServices := []string{"squest"} + + ua := initTemplate() + services := ua.GetServices() + + // Check if expected name and services are present + if ua.GetName() != "orchestration" { + t.Errorf("Name mismatch expected 'registry', got: %s", ua.GetName()) + } + + for _, s := range expectedServices { + if _, ok := services[s]; !ok { + t.Errorf("Expected service '%s' to be present", s) + } + } +} + func createTestServiceQuest() forms.ServiceQuest_v1 { var ServiceQuest_v1 forms.ServiceQuest_v1 ServiceQuest_v1.NewForm() From f96b562fbf258e2675655fec501d4209e4492bb3 Mon Sep 17 00:00:00 2001 From: gabaxh Date: Mon, 14 Jul 2025 16:52:23 +0200 Subject: [PATCH 22/81] Added the possibility to get several services from the orchestrator instead of just one --- orchestrator/extra_utils_test.go | 2 +- orchestrator/orchestrator.go | 50 ++++++++++- orchestrator/orchestrator_test.go | 88 ++++++++++++++++++-- orchestrator/thing.go | 79 +++++++++++++++++- orchestrator/thing_test.go | 133 ++++++++++++++++++++++++++++-- 5 files changed, 334 insertions(+), 18 deletions(-) diff --git a/orchestrator/extra_utils_test.go b/orchestrator/extra_utils_test.go index fff3ddb..82cc660 100644 --- a/orchestrator/extra_utils_test.go +++ b/orchestrator/extra_utils_test.go @@ -46,7 +46,7 @@ func createSystemWithUnitAsset(url string) components.System { leadingRegistrar := &components.CoreSystem{ Name: components.ServiceRegistrarName, - Url: "https://leadingregistrar", + Url: "http://localhost:20102/serviceregistrar/registry", } sys.CoreS = []*components.CoreSystem{ leadingRegistrar, diff --git a/orchestrator/orchestrator.go b/orchestrator/orchestrator.go index 0da1942..6b8f010 100644 --- a/orchestrator/orchestrator.go +++ b/orchestrator/orchestrator.go @@ -96,7 +96,8 @@ func (ua *UnitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath switch servicePath { case "squest": ua.orchestrate(w, r) - + case "squests": + ua.orchestrateMultiple(w, r) default: http.Error(w, "Invalid service request [Do not modify the services subpath in the configuration file]", http.StatusBadRequest) } @@ -149,3 +150,50 @@ func (ua *UnitAsset) orchestrate(w http.ResponseWriter, r *http.Request) { http.Error(w, "Method is not supported.", http.StatusNotFound) } } + +func (ua *UnitAsset) orchestrateMultiple(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "POST": + contentType := r.Header.Get("Content-Type") + mediaType, _, err := mime.ParseMediaType(contentType) + if err != nil { + fmt.Println("Error parsing media type:", err) + return + } + + defer r.Body.Close() + bodyBytes, err := io.ReadAll(r.Body) // Use io.ReadAll instead of ioutil.ReadAll + if err != nil { + log.Printf("error reading discovery request body: %v\n", err) + return + } + + questForm, err := usecases.Unpack(bodyBytes, mediaType) + if err != nil { + log.Printf("error extracting the discovery request %v\n", err) + } + // Perform a type assertion to convert the returned Form to SignalA_v1a + qf, ok := questForm.(*forms.ServiceQuest_v1) + if !ok { + fmt.Println("Problem unpacking the service discovery request form") + return + } + + servLocation, err := ua.getServicesURL(*qf) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusServiceUnavailable) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err = w.Write(servLocation) // respond with the selected service location + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + default: + http.Error(w, "Method is not supported.", http.StatusNotFound) + } +} diff --git a/orchestrator/orchestrator_test.go b/orchestrator/orchestrator_test.go index 1eb5acc..90222cf 100644 --- a/orchestrator/orchestrator_test.go +++ b/orchestrator/orchestrator_test.go @@ -14,10 +14,11 @@ import ( func TestServing(t *testing.T) { inputW := httptest.NewRecorder() - inputR := httptest.NewRequest(http.MethodPost, "/test123", io.NopCloser(strings.NewReader(string(createTestServiceQuestForm())))) + inputR := httptest.NewRequest(http.MethodPost, "/test123", + io.NopCloser(strings.NewReader(string(createTestServiceQuestForm())))) inputR.Header.Set("Content-Type", "application/json") newMockTransport(createMultiHTTPResponse(2, false, string(createTestServiceRecordListForm())), 0, nil) - mua := createUnitAsset("https://leadingregistrar") + mua := createUnitAsset("http://localhost:20102/serviceregistrar") mua.Serving(inputW, inputR, "squest") var expectedOutput = string(createTestServicePointForm()) @@ -27,11 +28,26 @@ func TestServing(t *testing.T) { expectedOutput, 200, inputW.Body.String(), inputW.Code) } + inputW = httptest.NewRecorder() + inputR = httptest.NewRequest(http.MethodPost, "/test123", + io.NopCloser(strings.NewReader(string(createTestServiceQuestForm())))) + inputR.Header.Set("Content-Type", "application/json") + newMockTransport(createMultiHTTPResponse(2, false, string(createTestServiceRecordListForm())), 0, nil) + mua = createUnitAsset("http://localhost:20102/serviceregistrar") + mua.Serving(inputW, inputR, "squests") + + expectedOutput = string(createTestServiceRecordListForm()) + + if inputW.Body.String() != expectedOutput || inputW.Code != 200 { + t.Errorf("Expected %s and code %d, got: %s and code %d", + expectedOutput, 200, inputW.Body.String(), inputW.Code) + } + inputW = httptest.NewRecorder() inputR = httptest.NewRequest(http.MethodPost, "/test123", io.NopCloser(strings.NewReader(""))) inputR.Header.Set("Content-Type", "application/json") newMockTransport(createMultiHTTPResponse(2, false, string(createTestServiceRecordListForm())), 0, nil) - mua = createUnitAsset("https://leadingregistrar") + mua = createUnitAsset("http://localhost:20102/serviceregistrar") mua.Serving(inputW, inputR, "wrong") if inputW.Code == 200 { @@ -93,15 +109,20 @@ func createTestServicePointForm() []byte { var serviceRecordForm forms.ServiceRecord_v1 +var serviceRecord2Form forms.ServiceRecord_v1 + var serviceRecordListForm forms.ServiceRecordList_v1 func createTestServiceRecordListForm() []byte { serviceRecordForm.NewForm() serviceRecordForm.IPAddresses = []string{"123.456.789"} serviceRecordForm.ProtoPort = map[string]int{"http": 123} + serviceRecord2Form.NewForm() + serviceRecord2Form.IPAddresses = []string{"123.456.789"} + serviceRecord2Form.ProtoPort = map[string]int{"http": 123} serviceRecordListForm.NewForm() - serviceRecordListForm.List = []forms.ServiceRecord_v1{serviceRecordForm} - fakebody, err := json.Marshal(serviceRecordListForm) + serviceRecordListForm.List = []forms.ServiceRecord_v1{serviceRecordForm, serviceRecord2Form} + fakebody, err := json.MarshalIndent(serviceRecordListForm, "", " ") if err != nil { log.Fatalf("Fail marshal at start of test: %v", err) } @@ -109,8 +130,8 @@ func createTestServiceRecordListForm() []byte { } var getServiceURLErrorMessage = "core system 'serviceregistrar' not found: verifying core URL: Get " + - "\"https://leadingregistrar/status\": http: RoundTripper implementation (*main.mockTransport) returned a nil " + - "*Response with a nil error\n" + "\"http://localhost:20102/serviceregistrar/registry/status\": http: RoundTripper implementation " + + "(*main.mockTransport) returned a nil *Response with a nil error\n" type orchestrateTestStruct struct { inputW http.ResponseWriter @@ -144,7 +165,7 @@ func TestOrchestrate(t *testing.T) { for _, testCase := range orchestrateTestParams { inputR := httptest.NewRequest(testCase.httpMethod, "/test123", testCase.inputBody) inputR.Header.Set("Content-Type", testCase.contentType) - mua := createUnitAsset("https://leadingregistrar") + mua := createUnitAsset("http://localhost:20102/serviceregistrar/registry") newMockTransport(createMultiHTTPResponse(2, false, string(createTestServiceRecordListForm())), testCase.mockTransportErr, nil) mua.orchestrate(testCase.inputW, inputR) @@ -162,3 +183,54 @@ func TestOrchestrate(t *testing.T) { } } } + +type orchestrateMultipleTestStruct struct { + inputW http.ResponseWriter + inputBody io.ReadCloser + httpMethod string + contentType string + mockTransportErr int + expectedCode int + expectedOutput string + testName string +} + +var orchestrateMultipleTestParams = []orchestrateMultipleTestStruct{ + {httptest.NewRecorder(), io.NopCloser(strings.NewReader(string(createTestServiceQuestForm()))), "POST", + "application/json", 3, 200, string(createTestServiceRecordListForm()), "Best case, everything passes"}, + {httptest.NewRecorder(), io.NopCloser(strings.NewReader(string(createTestServiceQuestForm()))), "POST", + "", 3, 200, "", "Bad case, header content type is wrong"}, + {httptest.NewRecorder(), io.NopCloser(errorReader{}), "POST", + "application/json", 3, 200, "", "Bad case, ReadAll on header body fails"}, + {httptest.NewRecorder(), io.NopCloser(strings.NewReader(string("hej hej"))), "POST", + "text/plain", 3, 200, "", "Bad case, Unpack and type assertion to ServiceQuest_v1 fails"}, + {httptest.NewRecorder(), io.NopCloser(strings.NewReader(string(createTestServiceQuestForm()))), "POST", + "application/json", 1, 503, getServiceURLErrorMessage, "Bad case, getServiceURL fails"}, + {&mockResponseWriter{}, io.NopCloser(strings.NewReader(string(createTestServiceQuestForm()))), "POST", + "application/json", 3, 0, "", "Bad case, write fails"}, + {httptest.NewRecorder(), io.NopCloser(strings.NewReader(string(""))), "PUT", + "", 0, 404, "Method is not supported.\n", "Bad case, wrong http method"}, +} + +func TestOrchestrateMultiple(t *testing.T) { + for _, testCase := range orchestrateMultipleTestParams { + inputR := httptest.NewRequest(testCase.httpMethod, "/test123", testCase.inputBody) + inputR.Header.Set("Content-Type", testCase.contentType) + mua := createUnitAsset("http://localhost:20102/serviceregistrar/registry") + newMockTransport(createMultiHTTPResponse(2, false, string(createTestServiceRecordListForm())), + testCase.mockTransportErr, nil) + mua.orchestrateMultiple(testCase.inputW, inputR) + + recorder, ok := testCase.inputW.(*httptest.ResponseRecorder) + if ok { + if recorder.Body.String() != testCase.expectedOutput || recorder.Code != testCase.expectedCode { + t.Errorf("In test case: %s: Expected %s, got: %s", + testCase.testName, testCase.expectedOutput, recorder.Body.String()) + } + } else { + if _, ok := testCase.inputW.(*mockResponseWriter); !ok { + t.Errorf("Expected inputW to be of type mockResponseWriter") + } + } + } +} diff --git a/orchestrator/thing.go b/orchestrator/thing.go index 7fe6790..9ca1dc1 100644 --- a/orchestrator/thing.go +++ b/orchestrator/thing.go @@ -87,6 +87,12 @@ func initTemplate() components.UnitAsset { Details: map[string][]string{"DefaultForm": {"ServiceRecord_v1"}, "Location": {"LocalCloud"}}, Description: "looks for the desired service described in a quest form (POST)", } + squests := components.Service{ + Definition: "squests", + SubPath: "squests", + Details: map[string][]string{"DefaultForm": {"ServiceRecord_v1"}, "Location": {"LocalCloud"}}, + Description: "looks for the desired services described in a quest form (POST)", + } assetTraits := Traits{ leadingRegistrar: nil, // Initialize the leading registrar to nil @@ -98,7 +104,8 @@ func initTemplate() components.UnitAsset { Details: map[string][]string{"Platform": {"Independent"}}, Traits: assetTraits, ServicesMap: components.Services{ - squest.SubPath: &squest, // Inline assignment of the temperature service + squest.SubPath: &squest, // Inline assignment of the temperature service + squests.SubPath: &squests, }, } return uat @@ -158,8 +165,8 @@ func UnmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { // - servLoc: A byte slice containing the service location in JSON format. // - err: An error if any issues occur during the process. func (ua *UnitAsset) getServiceURL(newQuest forms.ServiceQuest_v1) (servLoc []byte, err error) { - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) // Create a new context, with a 2-second timeout - // ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second) // Create a new context, with a 2-second timeout + // ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) // Create a new context, with a 2-second timeout + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second) // Create a new context, with a 2-second timeout defer cancel() sys := ua.Owner ua.leadingRegistrar.Url, err = components.GetRunningCoreSystemURL(sys, ua.leadingRegistrar.Name) @@ -234,3 +241,69 @@ func selectService(serviceList forms.ServiceRecordList_v1) (sp forms.ServicePoin sp.ServNode = rec.ServiceNode return } + +func (ua *UnitAsset) getServicesURL(newQuest forms.ServiceQuest_v1) (servLoc []byte, err error) { + // ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) // Create a new context, with a 2-second timeout + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second) // Create a new context, with a 2-second timeout + defer cancel() + sys := ua.Owner + ua.leadingRegistrar.Url, err = components.GetRunningCoreSystemURL(sys, ua.leadingRegistrar.Name) + if err != nil { + return servLoc, err + } + + // Create a new HTTP request to the the Service Registrar + + // Create buffer to save a copy of the request body + mediaType := "application/json" + jsonQF, err := usecases.Pack(&newQuest, mediaType) + if err != nil { + log.Printf("problem encountered when marshalling the service quest\n") + return servLoc, err + } + + srURL := ua.leadingRegistrar.Url + "/query" + req, err := http.NewRequest(http.MethodPost, srURL, bytes.NewBuffer(jsonQF)) + if err != nil { + return servLoc, err + } + req.Header.Set("Content-Type", mediaType) // set the Content-Type header + req = req.WithContext(ctx) // associate the cancellable context with the request + + // forward the request to the leading Service Registrar///////////////////////////////// + // client := &http.Client{} + resp, err := http.DefaultClient.Do(req) + if err != nil { + ua.leadingRegistrar = nil + return servLoc, err + } + defer resp.Body.Close() + respBytes, err := io.ReadAll(resp.Body) + if err != nil { + log.Printf("Error reading discovery response body: %v", err) + return servLoc, err + } + fmt.Printf("\n%v\n", string(respBytes)) + serviceListf, err := usecases.Unpack(respBytes, mediaType) + if err != nil { + log.Print("Error extracting discovery reply ", err) + return servLoc, err + } + + // Perform a type assertion to convert the returned Form to SignalA_v1a + serviceList, ok := serviceListf.(*forms.ServiceRecordList_v1) + if !ok { + log.Println("problem asserting the type of the service list form") + return + } + + if len(serviceList.List) == 0 { + err = fmt.Errorf("unable to locate any such service: %s", newQuest.ServiceDefinition) + return + } + + fmt.Printf("/n the length of the service list is: %d\n", len(serviceList.List)) + payload, err := json.MarshalIndent(serviceList, "", " ") + fmt.Printf("the service location is %+v\n", serviceList) + return payload, err +} diff --git a/orchestrator/thing_test.go b/orchestrator/thing_test.go index 7078f58..43d0000 100644 --- a/orchestrator/thing_test.go +++ b/orchestrator/thing_test.go @@ -32,9 +32,11 @@ func TestInitTemplate(t *testing.T) { } func createTestServiceQuest() forms.ServiceQuest_v1 { - var ServiceQuest_v1 forms.ServiceQuest_v1 - ServiceQuest_v1.NewForm() - return ServiceQuest_v1 + var ServiceQuest_v1_temperature forms.ServiceQuest_v1 + ServiceQuest_v1_temperature.NewForm() + ServiceQuest_v1_temperature.ServiceDefinition = "temperature" + ServiceQuest_v1_temperature.Details = map[string][]string{"Unit": {"Celsius"}} + return ServiceQuest_v1_temperature } func (ua *UnitAsset) createDelayedBrokenURL(limit int) func() *http.Response { @@ -87,8 +89,6 @@ type getServiceURLTestStruct struct { var getServiceURLTestParams = []getServiceURLTestStruct{ {createTestServiceQuest(), string(createTestServiceRecordListForm()), "https://leadingregistrar", false, false, 0, nil, string(createTestServicePointForm()), false, "Good case, everything passes"}, - // {createTestServiceQuest(), string(createTestServiceRecordListForm()), "https://leadingregistrar", true, false, - // 0, nil, "", true, "Bad case, creating a new http request fails"}, {createTestServiceQuest(), string(createTestServiceRecordListForm()), "https://leadingregistrar", false, false, 2, errHTTP, "", true, "Bad case, DefaultClient.Do fails"}, {createTestServiceQuest(), string(createTestServiceRecordListForm()), "https://leadingregistrar", false, true, @@ -146,3 +146,126 @@ func TestSelectService(t *testing.T) { t.Errorf("Expected: %v, got: %v", expectedService, receivedService) } } + +func createTestServiceRecordListFormWithSeveral() []byte { + var serviceRecordFormTemperature forms.ServiceRecord_v1 + serviceRecordFormTemperature.NewForm() + serviceRecordFormTemperature.IPAddresses = []string{"123.456.789"} + serviceRecordFormTemperature.ProtoPort = map[string]int{"http": 123} + serviceRecordFormTemperature.ServiceDefinition = "temperature" + var serviceRecordFormRotation forms.ServiceRecord_v1 + serviceRecordFormRotation.NewForm() + serviceRecordFormRotation.IPAddresses = []string{"123.456.789"} + serviceRecordFormRotation.ProtoPort = map[string]int{"http": 123} + serviceRecordFormRotation.ServiceDefinition = "rotation" + var ServiceRecordListFormWithSeveral forms.ServiceRecordList_v1 + ServiceRecordListFormWithSeveral.NewForm() + ServiceRecordListFormWithSeveral.List = []forms.ServiceRecord_v1{serviceRecordFormTemperature, + serviceRecordFormRotation} + fakebody, err := json.MarshalIndent(ServiceRecordListFormWithSeveral, "", " ") + if err != nil { + log.Fatalf("Fail marshal at start of test: %v", err) + } + return fakebody +} + +func createTestServiceRecordListFormWithDefinition() []byte { + var serviceRecordFormWithDefinition forms.ServiceRecord_v1 + serviceRecordFormWithDefinition.NewForm() + serviceRecordFormWithDefinition.IPAddresses = []string{"123.456.789"} + serviceRecordFormWithDefinition.ProtoPort = map[string]int{"http": 123} + serviceRecordFormWithDefinition.ServiceDefinition = "temperature" + var serviceRecordListFormWithDefintion forms.ServiceRecordList_v1 + serviceRecordListFormWithDefintion.NewForm() + serviceRecordListFormWithDefintion.List = []forms.ServiceRecord_v1{serviceRecordFormWithDefinition} + fakebody, err := json.MarshalIndent(serviceRecordListFormWithDefintion, "", " ") + if err != nil { + log.Fatalf("Fail marshal at start of test: %v", err) + } + return fakebody +} + +func createTestServiceRecordListFormWithDetails() []byte { + var serviceRecordFormWithDetails forms.ServiceRecord_v1 + serviceRecordFormWithDetails.NewForm() + serviceRecordFormWithDetails.IPAddresses = []string{"123.456.789"} + serviceRecordFormWithDetails.ProtoPort = map[string]int{"http": 123} + serviceRecordFormWithDetails.Details = map[string][]string{"Location": {"Kitchen"}} + var serviceRecordListFormWithDetails forms.ServiceRecordList_v1 + serviceRecordListFormWithDetails.NewForm() + serviceRecordListFormWithDetails.List = []forms.ServiceRecord_v1{serviceRecordFormWithDetails} + fakebody, err := json.MarshalIndent(serviceRecordListFormWithDetails, "", " ") + if err != nil { + log.Fatalf("Fail marshal at start of test: %v", err) + } + return fakebody +} + +type getServicesURLTestStruct struct { + inputForm forms.ServiceQuest_v1 + inputBody string + leadingRegistrarUrl string + brokenUrl bool + writeError bool + mockTransportErr int + errHTTP error + expectedOutput string + expectedErr bool + testName string +} + +var getServicesURLTestParams = []getServicesURLTestStruct{ + {createTestServiceQuest(), string(createTestServiceRecordListFormWithSeveral()), + "http://localhost:20102/serviceregistrar", false, false, 0, nil, + string(createTestServiceRecordListFormWithSeveral()), false, + "Good case, everything passes with several services"}, + {createTestServiceQuest(), string(createTestServiceRecordListFormWithDefinition()), + "http://localhost:20102/serviceregistrar", false, false, 0, nil, + string(createTestServiceRecordListFormWithDefinition()), false, + "Good case, everything passes with one service definition"}, + {createTestServiceQuest(), string(createTestServiceRecordListFormWithDetails()), + "http://localhost:20102/serviceregistrar", false, false, 0, nil, + string(createTestServiceRecordListFormWithDetails()), false, + "Good case, everything passes with one service details"}, + {createTestServiceQuest(), string(createTestServiceRecordListForm()), + "http://localhost:20102/serviceregistrar", false, false, 2, errHTTP, + "", true, + "Bad case, DefaultClient.Do fails"}, + {createTestServiceQuest(), string(createTestServiceRecordListForm()), + "http://localhost:20102/serviceregistrar", false, true, 0, nil, + "", true, + "Bad case, ReadAll fails"}, + {createTestServiceQuest(), "hej hej", + "http://localhost:20102/serviceregistrar", false, false, 0, nil, + "", true, + "Bad case, Unpack fails"}, + {createTestServiceQuest(), string(createTestServicePointForm()), + "http://localhost:20102/serviceregistrar", false, false, 0, nil, + "", false, + "Bad case, type assertion fails"}, + {createTestServiceQuest(), string(createEmptyServiceRecordListForm()), + "http://localhost:20102/serviceregistrar", false, false, 0, nil, + "", true, + "Bad case, the service record list is empty"}, +} + +func TestGetServicesURL(t *testing.T) { + for _, testCase := range getServicesURLTestParams { + mua := createUnitAsset(testCase.leadingRegistrarUrl) + if mua == nil { + t.Fatalf("UAssets[\"Orchestration\"] is nil") + } + if testCase.brokenUrl == true { + newMockTransport(mua.createDelayedBrokenURL(2), testCase.mockTransportErr, testCase.errHTTP) + } else { + newMockTransport(createMultiHTTPResponse(2, testCase.writeError, testCase.inputBody), + testCase.mockTransportErr, testCase.errHTTP) + } + servLoc, err := mua.getServicesURL(testCase.inputForm) + if string(servLoc) != testCase.expectedOutput || (err == nil && testCase.expectedErr == true) || + (err != nil && testCase.expectedErr == false) { + t.Errorf("In test case: %s: Expected %s and error %t, got: %s and %v", + testCase.testName, testCase.expectedOutput, testCase.expectedErr, string(servLoc), err) + } + } +} From cb01aa6e8a6ea6983b44c14a7d5957d6a256ed33 Mon Sep 17 00:00:00 2001 From: gabaxh Date: Tue, 15 Jul 2025 16:37:08 +0200 Subject: [PATCH 23/81] Continued working on tests of orchestrator system --- orchestrator/extra_utils_test.go | 5 +---- orchestrator/orchestrator_test.go | 2 +- orchestrator/thing.go | 28 ++++++++++++++++------------ orchestrator/thing_test.go | 2 +- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/orchestrator/extra_utils_test.go b/orchestrator/extra_utils_test.go index 82cc660..4c5145c 100644 --- a/orchestrator/extra_utils_test.go +++ b/orchestrator/extra_utils_test.go @@ -64,10 +64,7 @@ func createUnitAsset(url string) *UnitAsset { } assetTraits := Traits{ - leadingRegistrar: &components.CoreSystem{ - Name: components.ServiceRegistrarName, - Url: url, - }, + leadingRegistrar: "", } // create the unit asset template diff --git a/orchestrator/orchestrator_test.go b/orchestrator/orchestrator_test.go index 90222cf..b2c325f 100644 --- a/orchestrator/orchestrator_test.go +++ b/orchestrator/orchestrator_test.go @@ -129,7 +129,7 @@ func createTestServiceRecordListForm() []byte { return fakebody } -var getServiceURLErrorMessage = "core system 'serviceregistrar' not found: verifying core URL: Get " + +var getServiceURLErrorMessage = "core system 'serviceregistrar' not found: verifying registrar: Get " + "\"http://localhost:20102/serviceregistrar/registry/status\": http: RoundTripper implementation " + "(*main.mockTransport) returned a nil *Response with a nil error\n" diff --git a/orchestrator/thing.go b/orchestrator/thing.go index 9ca1dc1..e2c66b4 100644 --- a/orchestrator/thing.go +++ b/orchestrator/thing.go @@ -34,7 +34,7 @@ import ( // Traits are Asset-specific configurable parameters and variables type Traits struct { - leadingRegistrar *components.CoreSystem + leadingRegistrar string } // UnitAsset type models the unit asset (interface) of the system. @@ -95,7 +95,7 @@ func initTemplate() components.UnitAsset { } assetTraits := Traits{ - leadingRegistrar: nil, // Initialize the leading registrar to nil + leadingRegistrar: "", // Initialize the leading registrar to nil } // create the unit asset template @@ -169,9 +169,11 @@ func (ua *UnitAsset) getServiceURL(newQuest forms.ServiceQuest_v1) (servLoc []by ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second) // Create a new context, with a 2-second timeout defer cancel() sys := ua.Owner - ua.leadingRegistrar.Url, err = components.GetRunningCoreSystemURL(sys, ua.leadingRegistrar.Name) - if err != nil { - return servLoc, err + if ua.leadingRegistrar == "" { + ua.leadingRegistrar, err = components.GetRunningCoreSystemURL(sys, "serviceregistrar") + if err != nil { + return servLoc, err + } } // Create a new HTTP request to the the Service Registrar @@ -184,7 +186,7 @@ func (ua *UnitAsset) getServiceURL(newQuest forms.ServiceQuest_v1) (servLoc []by return servLoc, err } - srURL := ua.leadingRegistrar.Url + "/query" + srURL := ua.leadingRegistrar + "/query" req, err := http.NewRequest(http.MethodPost, srURL, bytes.NewBuffer(jsonQF)) if err != nil { return servLoc, err @@ -196,7 +198,7 @@ func (ua *UnitAsset) getServiceURL(newQuest forms.ServiceQuest_v1) (servLoc []by // client := &http.Client{} resp, err := http.DefaultClient.Do(req) if err != nil { - ua.leadingRegistrar = nil + ua.leadingRegistrar = "" return servLoc, err } defer resp.Body.Close() @@ -247,9 +249,11 @@ func (ua *UnitAsset) getServicesURL(newQuest forms.ServiceQuest_v1) (servLoc []b ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second) // Create a new context, with a 2-second timeout defer cancel() sys := ua.Owner - ua.leadingRegistrar.Url, err = components.GetRunningCoreSystemURL(sys, ua.leadingRegistrar.Name) - if err != nil { - return servLoc, err + if ua.leadingRegistrar == "" { + ua.leadingRegistrar, err = components.GetRunningCoreSystemURL(sys, "serviceregistrar") + if err != nil { + return servLoc, err + } } // Create a new HTTP request to the the Service Registrar @@ -262,7 +266,7 @@ func (ua *UnitAsset) getServicesURL(newQuest forms.ServiceQuest_v1) (servLoc []b return servLoc, err } - srURL := ua.leadingRegistrar.Url + "/query" + srURL := ua.leadingRegistrar + "/query" req, err := http.NewRequest(http.MethodPost, srURL, bytes.NewBuffer(jsonQF)) if err != nil { return servLoc, err @@ -274,7 +278,7 @@ func (ua *UnitAsset) getServicesURL(newQuest forms.ServiceQuest_v1) (servLoc []b // client := &http.Client{} resp, err := http.DefaultClient.Do(req) if err != nil { - ua.leadingRegistrar = nil + ua.leadingRegistrar = "" return servLoc, err } defer resp.Body.Close() diff --git a/orchestrator/thing_test.go b/orchestrator/thing_test.go index 43d0000..12eb84e 100644 --- a/orchestrator/thing_test.go +++ b/orchestrator/thing_test.go @@ -45,7 +45,7 @@ func (ua *UnitAsset) createDelayedBrokenURL(limit int) func() *http.Response { count++ if count == limit { f := createTestServiceRecordListForm() - ua.leadingRegistrar.Url = brokenUrl + ua.leadingRegistrar = brokenUrl return &http.Response{ Status: "200 OK", StatusCode: 200, From 67128da5b2aee45e6922d47b34d4e459431d1844 Mon Sep 17 00:00:00 2001 From: gabaxh Date: Wed, 16 Jul 2025 17:01:13 +0200 Subject: [PATCH 24/81] Continued with tests on orchestrator system --- orchestrator/extra_utils_test.go | 31 ++- orchestrator/integration_test.go | 413 ++++++++++++++++++++++++++++++ orchestrator/orchestrator_test.go | 13 +- orchestrator/thing_test.go | 56 +++- 4 files changed, 499 insertions(+), 14 deletions(-) create mode 100644 orchestrator/integration_test.go diff --git a/orchestrator/extra_utils_test.go b/orchestrator/extra_utils_test.go index 4c5145c..cc762ff 100644 --- a/orchestrator/extra_utils_test.go +++ b/orchestrator/extra_utils_test.go @@ -140,25 +140,40 @@ func (errorReader) Read(p []byte) (int, error) { } type mockResponseWriter struct { - http.ResponseWriter + headers http.Header + body []byte + status int + writeError bool } func (e *mockResponseWriter) Write(b []byte) (int, error) { - return 0, fmt.Errorf("Forced write error") + if e.writeError { + return 0, fmt.Errorf("Forced write error") + } + e.body = append(e.body, b...) + return len(b), nil } -func (e *mockResponseWriter) WriteHeader(statusCode int) {} +func (e *mockResponseWriter) WriteHeader(statusCode int) { + e.status = statusCode +} func (e *mockResponseWriter) Header() http.Header { - return make(http.Header) + return e.headers +} + +func newMockResponseWriter() *mockResponseWriter { + return &mockResponseWriter{ + headers: make(http.Header), + status: http.StatusOK, + writeError: true, + } } var brokenUrl = string(rune(0)) var errHTTP error = fmt.Errorf("bad http request") -/* - // Create a error reader to break json.Unmarshal() type errReader int @@ -211,7 +226,7 @@ func createTestSystem(broken bool) (sys components.System) { setTest.SubPath: setTest, } assetTraits := Traits{ - leadingRegistrar: nil, + leadingRegistrar: "", } mua := &UnitAsset{ Name: "testUnitAsset", @@ -256,5 +271,3 @@ func createTestSystem(broken bool) (sys components.System) { } return } - -*/ diff --git a/orchestrator/integration_test.go b/orchestrator/integration_test.go new file mode 100644 index 0000000..ebd793f --- /dev/null +++ b/orchestrator/integration_test.go @@ -0,0 +1,413 @@ +package main + +/* + +type requestEvent struct { + event string + hits int + body []byte +} + +// Mock simulating traffic between a system and registrars/orchestrators +type mockTrans struct { + t *testing.T + hits map[string]int // Used to track http requests + mutex sync.Mutex // For protecting access to the above map + events chan requestEvent // Tracks service "events" and requests to the cloud services +} + +func newIntegrationMockTransport(t *testing.T) *mockTrans { + m := &mockTrans{ + t: t, + hits: make(map[string]int), + events: make(chan requestEvent), + } + // Hijack the default http client so no actual http requests are sent over the network + http.DefaultClient.Transport = m + return m +} + +func (m *mockTrans) waitFor(event string) (int, []byte, error) { + select { + case e := <-m.events: + if e.event != event { + return 0, nil, fmt.Errorf("got %s, expected %s", e.event, event) + } + return e.hits, e.body, nil + case <-time.Tick(10 * time.Second): + return 0, nil, fmt.Errorf("event timeout") + } +} + +func newServiceRecord() []byte { + f := forms.ServiceRecord_v1{ + Id: 13, // NOTE: this should match with eventUnregister + Created: time.Now().Format(time.RFC3339), + EndOfValidity: time.Now().Format(time.RFC3339), + Version: "ServiceRecord_v1", + } + b, err := usecases.Pack(&f, "application/json") + if err != nil { + panic(err) // Hard fail if Pack() can't handle the above form + } + return b +} + +func newServicePoint() []byte { + f := forms.ServicePoint_v1{ + // per usecases/registration.go:serviceRegistrationForm() + ServNode: fmt.Sprintf("localhost_%s_%s_%s", systemName, unitName, unitService), + // per orchestrator/thing.go:selectService() + ServLocation: fmt.Sprintf("http://localhost:%d/%s/%s/%s", + systemPort, systemName, unitName, unitService, + ), + Version: "ServicePoint_v1", + } + b, err := usecases.Pack(&f, "application/json") + if err != nil { + panic(err) // Another hard fail if Pack() can't work with the above form + } + return b +} + +const ( + eventRegistryStatus string = "GET /serviceregistrar/registry/status" + eventRegister string = "POST /serviceregistrar/registry/register" + eventUnregister string = "DELETE /serviceregistrar/registry/unregister/13" + eventOrchestration string = "GET /orchestrator/orchestration" + eventOrchestrate string = "POST /orchestrator/orchestration/squest" +) + +var mockRequests = map[string]struct { + sendEvent bool + status int + body []byte +}{ + eventRegistryStatus: {false, 200, []byte(components.ServiceRegistrarLeader)}, + eventRegister: {true, 200, newServiceRecord()}, + eventUnregister: {true, 200, nil}, + eventOrchestration: {false, 200, nil}, + eventOrchestrate: {true, 200, newServicePoint()}, +} + +func (m *mockTrans) RoundTrip(req *http.Request) (*http.Response, error) { + m.mutex.Lock() // This lock is mainly for guarding concurrent access to the hits map + defer m.mutex.Unlock() + event := req.Method + " " + req.URL.Path + m.hits[event] += 1 + if event == serviceURL { + // The example service will, through the system, return a proper response + return http.DefaultTransport.RoundTrip(req) + } + + // Any other requests needs to be mocked, simulating responses from the + // service registrar and orchestrator. + mock, found := mockRequests[event] + if !found { + m.t.Errorf("unknown request: %s", event) + // Let's see how the system responds to this + mock.status = http.StatusNotImplemented + mock.body = []byte(http.StatusText(mock.status)) + } + rec := httptest.NewRecorder() + rec.Header().Set("Content-Type", "application/json") + rec.WriteHeader(mock.status) + rec.Write(mock.body) // Safe to ignore the returned error, it's always nil + + // Allows for syncing up the test, with the request flow performed by the system + if mock.sendEvent { + var b []byte + if req.Body != nil { + var err error + b, err = io.ReadAll(req.Body) + if err != nil { + m.t.Errorf("failed reading request body: %v", err) + } + defer req.Body.Close() + } + // Using a goroutine prevents thread locking + go func(e string, h int, b []byte) { + m.events <- requestEvent{e, h, b} + }(event, m.hits[event], b) + } + return rec.Result(), nil +} + +//////////////////////////////////////////////////////////////////////////////// + +func countGoroutines() (int, string) { + c := runtime.NumGoroutine() + buf := &bytes.Buffer{} + // A write to this buffer will always return nil error, so safe to ignore here. + // This call will spawn some goroutine too, so need to chill for a little while. + _ = pprof.Lookup("goroutine").WriteTo(buf, 2) + trace := buf.String() + // Calling signal.Notify() will leave an extra goroutine that runs forever, + // so it should be subtracted from the count. For more info, see: + // https://github.com/golang/go/issues/52619 + // https://github.com/golang/go/issues/72803 + // https://github.com/golang/go/issues/21576 + if strings.Contains(trace, "os/signal.signal_recv") { + c -= 1 + } + return c, trace +} + +func assertNotEq(t *testing.T, got, want any) { + if got != want { + t.Errorf("got %v, expected %v", got, want) + } +} + +func TestSimpleSystemIntegration(t *testing.T) { + routinesStart, _ := countGoroutines() + m := newIntegrationMockTransport(t) + sys, stopSystem, err := newSystem() + if err != nil { + t.Fatalf("expected no error, got: %s", err) + } + + // Validate service registration + hits, body, err := m.waitFor(eventRegister) + assertNotEq(t, err, nil) + if hits != 1 { + t.Errorf("system skipped: %s", eventRegister) + } + var sr forms.ServiceRecord_v1 + err = json.Unmarshal(body, &sr) + assertNotEq(t, err, nil) + assertNotEq(t, sr.SystemName, systemName) + assertNotEq(t, sr.SubPath, path.Join(unitName, unitService)) + + // Validate service usage + ua := *sys.UAssets[unitName] + if ua == nil { + t.Fatalf("system missing unit asset: %s", unitName) + } + service := ua.GetCervices()[unitService] + if service == nil { + t.Fatalf("unit asset missing cervice: %s", unitService) + } + f, err := usecases.GetState(service, sys) + assertNotEq(t, err, nil) + fs, ok := f.(*forms.SignalA_v1a) + if ok == false || fs == nil || fs.Value == 0.0 { + t.Errorf("invalid form: %#v", f) + } + + // Late validation for service discovery + hits, body, err = m.waitFor(eventOrchestrate) + assertNotEq(t, err, nil) + if hits != 1 { + t.Errorf("system skipped: %s", eventUnregister) + } + var sq forms.ServiceQuest_v1 + err = json.Unmarshal(body, &sq) + assertNotEq(t, err, nil) + assertNotEq(t, sq.ServiceDefinition, unitService) + + // Validate service unregister + stopSystem() + hits, _, err = m.waitFor(eventUnregister) // NOTE: doesn't receive a body + assertNotEq(t, err, nil) + if hits != 1 { + t.Errorf("system skipped: %s", eventUnregister) + } + + // Detect any leaking goroutines + // Delay a short moment and let the goroutines finish. Not sure if there's + // a better way to wait for an _unknown number_ of goroutines. + // This might give flaky test results in slower environments! + time.Sleep(1 * time.Second) + routinesStop, trace := countGoroutines() + if (routinesStop - routinesStart) != 0 { + t.Errorf("leaking goroutines: count at start=%d, stop=%d\n%s", + routinesStart, routinesStop, trace, + ) + } +} + +const ( + unitName string = "randomiser" + unitService string = "random" +) + +// The most simplest unit asset +type uaRandomiser struct { + Name string `json:"-"` + Owner *components.System `json:"-"` + Details map[string][]string `json:"-"` + ServicesMap components.Services `json:"-"` + CervicesMap components.Cervices `json:"-"` +} + +// Force type check (fulfilling the interface) at compile time +var _ components.UnitAsset = &uaRandomiser{} + +// Add required functions to fulfil the UnitAsset interface +func (ua uaRandomiser) GetName() string { return ua.Name } +func (ua uaRandomiser) GetServices() components.Services { return ua.ServicesMap } +func (ua uaRandomiser) GetCervices() components.Cervices { return ua.CervicesMap } +func (ua uaRandomiser) GetDetails() map[string][]string { return ua.Details } + +func (ua uaRandomiser) Serving(w http.ResponseWriter, r *http.Request, servicePath string) { + if servicePath != unitService { + http.Error(w, "unknown service path: "+servicePath, http.StatusBadRequest) + return + } + + f := forms.SignalA_v1a{ + Value: rand.Float64(), + } + b, err := usecases.Pack(f.NewForm(), "application/json") + if err != nil { + http.Error(w, "error from Pack: "+err.Error(), http.StatusInternalServerError) + return + } + if _, err := w.Write(b); err != nil { + http.Error(w, "error from Write: "+err.Error(), http.StatusInternalServerError) + } +} + +func createUATemplate(sys *components.System) { + s := &components.Service{ + Definition: unitService, // The "name" of the service + SubPath: unitService, // Not "allowed" to be changed afterwards + Details: map[string][]string{"key1": {"value1"}}, + RegPeriod: 60, + // NOTE: must start with lower-case, it gets embedded into another sentence in the web API + Description: "returns a random float64", + } + ua := components.UnitAsset(&uaRandomiser{ + Name: unitName, // WARN: don't use the system name!! this is an asset! + Details: map[string][]string{"key2": {"value2"}}, + ServicesMap: components.Services{ + s.SubPath: s, + }, + }) + sys.UAssets[ua.GetName()] = &ua +} + +func loadUAConfig(ca usecases.ConfigurableAsset, sys *components.System) (components.UnitAsset, func()) { + s := ca.Services[0] + ua := &uaRandomiser{ + Name: ca.Name, + Owner: sys, + Details: ca.Details, + ServicesMap: usecases.MakeServiceMap(ca.Services), + // Let it consume its own service + CervicesMap: components.Cervices{unitService: &components.Cervice{ + Definition: s.Definition, + Details: s.Details, + // Nodes will be filled up by any discovered cervices + Nodes: make(map[string][]string, 0), + }}, + } + return ua, func() {} +} + +//////////////////////////////////////////////////////////////////////////////// + +const ( + systemName string = "test" + systemPort int = 29999 +) + +var serviceURL = "GET /" + path.Join(systemName, unitName, unitService) + +// The most simplest system +func newSystem() (*components.System, func(), error) { + ctx, cancel := context.WithCancel(context.Background()) + + // TODO: want this to return a pointer type instead! + // easier to use and pointer is used all the time anyway down below + sys := components.NewSystem(systemName, ctx) + sys.Husk = &components.Husk{ + Description: " is the most simplest system possible", + Details: map[string][]string{"key3": {"value3"}}, + ProtoPort: map[string]int{"http": systemPort}, + } + + // Setup default config with default unit asset and values + createUATemplate(&sys) + rawResources, err := usecases.Configure(&sys) + + // Extra check to work around "created config" error. Not required normally! + if err != nil { + // Return errors not related to config creation + if errors.Is(err, usecases.ErrNewConfig) == false { + cancel() + return nil, nil, err + } + // Since Configure() created the config file, it must be cleaned up when this test is done! + defer os.Remove("systemconfig.json") + // Default config file was created, redo the func call to load the file + rawResources, err = usecases.Configure(&sys) + if err != nil { + cancel() + return nil, nil, err + } + } + // NOTE: if the config file already existed (thus the above error block didn't + // get to run), then the config file should be left alone and not removed! + + // Load unit assets defined in the config file + cleanups, err := LoadResources(&sys, rawResources, loadUAConfig) + if err != nil { + cancel() + return nil, nil, err + } + + // TODO: this is not ready for production yet? + // usecases.RequestCertificate(&sys) + + usecases.RegisterServices(&sys) + + // TODO: prints logs + usecases.SetoutServers(&sys) + + stop := func() { + cancel() + // TODO: a waitgroup or something should be used to make sure all goroutines have stopped + // Not doing much in the mock cleanups so this works fine for now...? + cleanups() + } + return &sys, stop, nil +} + +type NewResourceFunc func(usecases.ConfigurableAsset, *components.System) (components.UnitAsset, func()) + +// LoadResources loads all unit assets from rawRes (which was loaded from "systemconfig.json" file) +// and calls newResFunc repeatedly for each loaded asset. +// The fully loaded unit asset and an optional cleanup function are collected from +// newResFunc and are then attached to the sys system. +// LoadResources then returns a system cleanup function and an optional error. +// The error always originate from [json.Unmarshal]. +func LoadResources(sys *components.System, rawRes []json.RawMessage, newResFunc NewResourceFunc) (func(), error) { + // Resets this map so it can be filled with loaded unit assets (rather than templates) + sys.UAssets = make(map[string]*components.UnitAsset) + + var cleanups []func() + for _, raw := range rawRes { + var ca usecases.ConfigurableAsset + if err := json.Unmarshal(raw, &ca); err != nil { + return func() {}, err + } + + ua, f := newResFunc(ca, sys) + sys.UAssets[ua.GetName()] = &ua + cleanups = append(cleanups, f) + } + + doCleanups := func() { + for _, f := range cleanups { + f() + } + // Stops hijacking SIGINT and return signal control to user + signal.Stop(sys.Sigs) + close(sys.Sigs) + } + return doCleanups, nil +} + +*/ diff --git a/orchestrator/orchestrator_test.go b/orchestrator/orchestrator_test.go index b2c325f..7e875b9 100644 --- a/orchestrator/orchestrator_test.go +++ b/orchestrator/orchestrator_test.go @@ -155,8 +155,8 @@ var orchestrateTestParams = []orchestrateTestStruct{ "text/plain", 3, 200, "", "Bad case, Unpack and type assertion to ServiceQuest_v1 fails"}, {httptest.NewRecorder(), io.NopCloser(strings.NewReader(string(createTestServiceQuestForm()))), "POST", "application/json", 1, 503, getServiceURLErrorMessage, "Bad case, getServiceURL fails"}, - {&mockResponseWriter{}, io.NopCloser(strings.NewReader(string(createTestServiceQuestForm()))), "POST", - "application/json", 3, 0, "", "Bad case, write fails"}, + {newMockResponseWriter(), io.NopCloser(strings.NewReader(string(createTestServiceQuestForm()))), "POST", + "application/json", 3, 500, "", "Bad case, write fails"}, {httptest.NewRecorder(), io.NopCloser(strings.NewReader(string(""))), "PUT", "", 0, 404, "Method is not supported.\n", "Bad case, wrong http method"}, } @@ -168,6 +168,7 @@ func TestOrchestrate(t *testing.T) { mua := createUnitAsset("http://localhost:20102/serviceregistrar/registry") newMockTransport(createMultiHTTPResponse(2, false, string(createTestServiceRecordListForm())), testCase.mockTransportErr, nil) + testCase.inputW.Header() mua.orchestrate(testCase.inputW, inputR) recorder, ok := testCase.inputW.(*httptest.ResponseRecorder) @@ -177,7 +178,11 @@ func TestOrchestrate(t *testing.T) { testCase.testName, testCase.expectedOutput, recorder.Body.String()) } } else { - if _, ok := testCase.inputW.(*mockResponseWriter); !ok { + if recorder, ok := testCase.inputW.(*mockResponseWriter); ok { + if recorder.status != testCase.expectedCode { + t.Errorf("Expected status %d, got %d", testCase.expectedCode, recorder.status) + } + } else { t.Errorf("Expected inputW to be of type mockResponseWriter") } } @@ -206,7 +211,7 @@ var orchestrateMultipleTestParams = []orchestrateMultipleTestStruct{ "text/plain", 3, 200, "", "Bad case, Unpack and type assertion to ServiceQuest_v1 fails"}, {httptest.NewRecorder(), io.NopCloser(strings.NewReader(string(createTestServiceQuestForm()))), "POST", "application/json", 1, 503, getServiceURLErrorMessage, "Bad case, getServiceURL fails"}, - {&mockResponseWriter{}, io.NopCloser(strings.NewReader(string(createTestServiceQuestForm()))), "POST", + {newMockResponseWriter(), io.NopCloser(strings.NewReader(string(createTestServiceQuestForm()))), "POST", "application/json", 3, 0, "", "Bad case, write fails"}, {httptest.NewRecorder(), io.NopCloser(strings.NewReader(string(""))), "PUT", "", 0, 404, "Method is not supported.\n", "Bad case, wrong http method"}, diff --git a/orchestrator/thing_test.go b/orchestrator/thing_test.go index 12eb84e..65c5a21 100644 --- a/orchestrator/thing_test.go +++ b/orchestrator/thing_test.go @@ -9,12 +9,13 @@ import ( "strings" "testing" + "github.com/sdoque/mbaigo/components" "github.com/sdoque/mbaigo/forms" "github.com/sdoque/mbaigo/usecases" ) func TestInitTemplate(t *testing.T) { - expectedServices := []string{"squest"} + expectedServices := []string{"squest", "squests"} ua := initTemplate() services := ua.GetServices() @@ -31,6 +32,59 @@ func TestInitTemplate(t *testing.T) { } } +func createConfAssetBrokenTraits() usecases.ConfigurableAsset { + brokenTrait, _ := json.Marshal(errReader(0)) + uac := usecases.ConfigurableAsset{ + Name: "testOrchestrator", + Details: map[string][]string{"testDetail": {"detail1", "detail2"}}, + Services: []components.Service{}, + Traits: []json.RawMessage{json.RawMessage(brokenTrait)}, + } + return uac +} + +func createConfAssetMultipleTraits() usecases.ConfigurableAsset { + uac := usecases.ConfigurableAsset{ + Name: "testOrchestrator", + Details: map[string][]string{"testDetail": {"detail1", "detail2"}}, + Services: []components.Service{}, + Traits: []json.RawMessage{json.RawMessage(`{"recCount": 0}`), json.RawMessage(`{"leading": false}`)}, + } + return uac +} + +type newResourceParams struct { + setup func() components.System + confAsset func() usecases.ConfigurableAsset + testCase string +} + +func TestNewResource(t *testing.T) { + params := []newResourceParams{ + { + func() (sys components.System) { return createTestSystem(false) }, + func() (confAsset usecases.ConfigurableAsset) { return createConfAssetBrokenTraits() }, + "Case: unmarshal traits fails", + }, + { + func() (sys components.System) { return createTestSystem(false) }, + func() (confAsset usecases.ConfigurableAsset) { return createConfAssetMultipleTraits() }, + "Case: confAsset has multiple traits", + }, + } + + for _, c := range params { + sys := c.setup() + uac := c.confAsset() + + ua, shutdown := newResource(uac, &sys) + shutdown() + if ua.GetName() != "testOrchestrator" { + t.Errorf("Name mismatch, expected '%s' got '%s'", uac.Name, ua.GetName()) + } + } +} + func createTestServiceQuest() forms.ServiceQuest_v1 { var ServiceQuest_v1_temperature forms.ServiceQuest_v1 ServiceQuest_v1_temperature.NewForm() From c230b3ffece51a9e5cbc6ce406a72dff50f40b2d Mon Sep 17 00:00:00 2001 From: gabaxh Date: Fri, 18 Jul 2025 08:09:03 +0200 Subject: [PATCH 25/81] Done with unit tests of orchestrator system --- orchestrator/extra_utils_test.go | 6 +-- orchestrator/orchestrator_test.go | 10 ++-- orchestrator/thing_test.go | 78 ++++++++++++++----------------- 3 files changed, 42 insertions(+), 52 deletions(-) diff --git a/orchestrator/extra_utils_test.go b/orchestrator/extra_utils_test.go index cc762ff..8351788 100644 --- a/orchestrator/extra_utils_test.go +++ b/orchestrator/extra_utils_test.go @@ -40,7 +40,7 @@ func (t *mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err e return resp, nil } -func createSystemWithUnitAsset(url string) components.System { +func createSystemWithUnitAsset() components.System { ctx := context.Background() sys := components.NewSystem("testSystem", ctx) @@ -54,7 +54,7 @@ func createSystemWithUnitAsset(url string) components.System { return sys } -func createUnitAsset(url string) *UnitAsset { +func createUnitAsset() *UnitAsset { // Define the services that expose the capabilities of the unit asset(s) squest := components.Service{ Definition: "squest", @@ -77,7 +77,7 @@ func createUnitAsset(url string) *UnitAsset { }, } - sys := createSystemWithUnitAsset(url) + sys := createSystemWithUnitAsset() uat.Owner = &sys return uat diff --git a/orchestrator/orchestrator_test.go b/orchestrator/orchestrator_test.go index 7e875b9..9088ffd 100644 --- a/orchestrator/orchestrator_test.go +++ b/orchestrator/orchestrator_test.go @@ -18,7 +18,7 @@ func TestServing(t *testing.T) { io.NopCloser(strings.NewReader(string(createTestServiceQuestForm())))) inputR.Header.Set("Content-Type", "application/json") newMockTransport(createMultiHTTPResponse(2, false, string(createTestServiceRecordListForm())), 0, nil) - mua := createUnitAsset("http://localhost:20102/serviceregistrar") + mua := createUnitAsset() mua.Serving(inputW, inputR, "squest") var expectedOutput = string(createTestServicePointForm()) @@ -33,7 +33,7 @@ func TestServing(t *testing.T) { io.NopCloser(strings.NewReader(string(createTestServiceQuestForm())))) inputR.Header.Set("Content-Type", "application/json") newMockTransport(createMultiHTTPResponse(2, false, string(createTestServiceRecordListForm())), 0, nil) - mua = createUnitAsset("http://localhost:20102/serviceregistrar") + mua = createUnitAsset() mua.Serving(inputW, inputR, "squests") expectedOutput = string(createTestServiceRecordListForm()) @@ -47,7 +47,7 @@ func TestServing(t *testing.T) { inputR = httptest.NewRequest(http.MethodPost, "/test123", io.NopCloser(strings.NewReader(""))) inputR.Header.Set("Content-Type", "application/json") newMockTransport(createMultiHTTPResponse(2, false, string(createTestServiceRecordListForm())), 0, nil) - mua = createUnitAsset("http://localhost:20102/serviceregistrar") + mua = createUnitAsset() mua.Serving(inputW, inputR, "wrong") if inputW.Code == 200 { @@ -165,7 +165,7 @@ func TestOrchestrate(t *testing.T) { for _, testCase := range orchestrateTestParams { inputR := httptest.NewRequest(testCase.httpMethod, "/test123", testCase.inputBody) inputR.Header.Set("Content-Type", testCase.contentType) - mua := createUnitAsset("http://localhost:20102/serviceregistrar/registry") + mua := createUnitAsset() newMockTransport(createMultiHTTPResponse(2, false, string(createTestServiceRecordListForm())), testCase.mockTransportErr, nil) testCase.inputW.Header() @@ -221,7 +221,7 @@ func TestOrchestrateMultiple(t *testing.T) { for _, testCase := range orchestrateMultipleTestParams { inputR := httptest.NewRequest(testCase.httpMethod, "/test123", testCase.inputBody) inputR.Header.Set("Content-Type", testCase.contentType) - mua := createUnitAsset("http://localhost:20102/serviceregistrar/registry") + mua := createUnitAsset() newMockTransport(createMultiHTTPResponse(2, false, string(createTestServiceRecordListForm())), testCase.mockTransportErr, nil) mua.orchestrateMultiple(testCase.inputW, inputR) diff --git a/orchestrator/thing_test.go b/orchestrator/thing_test.go index 65c5a21..3b58d65 100644 --- a/orchestrator/thing_test.go +++ b/orchestrator/thing_test.go @@ -128,36 +128,35 @@ func createEmptyServiceRecordListForm() []byte { } type getServiceURLTestStruct struct { - inputForm forms.ServiceQuest_v1 - inputBody string - leadingRegistrarUrl string - brokenUrl bool - writeError bool - mockTransportErr int - errHTTP error - expectedOutput string - expectedErr bool - testName string + inputForm forms.ServiceQuest_v1 + inputBody string + brokenUrl bool + writeError bool + mockTransportErr int + errHTTP error + expectedOutput string + expectedErr bool + testName string } var getServiceURLTestParams = []getServiceURLTestStruct{ - {createTestServiceQuest(), string(createTestServiceRecordListForm()), "https://leadingregistrar", false, false, + {createTestServiceQuest(), string(createTestServiceRecordListForm()), false, false, 0, nil, string(createTestServicePointForm()), false, "Good case, everything passes"}, - {createTestServiceQuest(), string(createTestServiceRecordListForm()), "https://leadingregistrar", false, false, + {createTestServiceQuest(), string(createTestServiceRecordListForm()), false, false, 2, errHTTP, "", true, "Bad case, DefaultClient.Do fails"}, - {createTestServiceQuest(), string(createTestServiceRecordListForm()), "https://leadingregistrar", false, true, + {createTestServiceQuest(), string(createTestServiceRecordListForm()), false, true, 0, nil, "", true, "Bad case, ReadAll fails"}, - {createTestServiceQuest(), "hej hej", "https://leadingregistrar", false, false, + {createTestServiceQuest(), "hej hej", false, false, 0, nil, "", true, "Bad case, Unpack fails"}, - {createTestServiceQuest(), string(createTestServicePointForm()), "https://leadingregistrar", false, false, + {createTestServiceQuest(), string(createTestServicePointForm()), false, false, 0, nil, "", false, "Bad case, type assertion fails"}, - {createTestServiceQuest(), string(createEmptyServiceRecordListForm()), "https://leadingregistrar", false, false, + {createTestServiceQuest(), string(createEmptyServiceRecordListForm()), false, false, 0, nil, "", true, "Bad case, the service record list is empty"}, } func TestGetServiceURL(t *testing.T) { for _, testCase := range getServiceURLTestParams { - mua := createUnitAsset(testCase.leadingRegistrarUrl) + mua := createUnitAsset() if mua == nil { t.Fatalf("UAssets[\"Orchestration\"] is nil") } @@ -256,56 +255,47 @@ func createTestServiceRecordListFormWithDetails() []byte { } type getServicesURLTestStruct struct { - inputForm forms.ServiceQuest_v1 - inputBody string - leadingRegistrarUrl string - brokenUrl bool - writeError bool - mockTransportErr int - errHTTP error - expectedOutput string - expectedErr bool - testName string + inputForm forms.ServiceQuest_v1 + inputBody string + brokenUrl bool + writeError bool + mockTransportErr int + errHTTP error + expectedOutput string + expectedErr bool + testName string } var getServicesURLTestParams = []getServicesURLTestStruct{ - {createTestServiceQuest(), string(createTestServiceRecordListFormWithSeveral()), - "http://localhost:20102/serviceregistrar", false, false, 0, nil, + {createTestServiceQuest(), string(createTestServiceRecordListFormWithSeveral()), false, false, 0, nil, string(createTestServiceRecordListFormWithSeveral()), false, "Good case, everything passes with several services"}, - {createTestServiceQuest(), string(createTestServiceRecordListFormWithDefinition()), - "http://localhost:20102/serviceregistrar", false, false, 0, nil, + {createTestServiceQuest(), string(createTestServiceRecordListFormWithDefinition()), false, false, 0, nil, string(createTestServiceRecordListFormWithDefinition()), false, "Good case, everything passes with one service definition"}, - {createTestServiceQuest(), string(createTestServiceRecordListFormWithDetails()), - "http://localhost:20102/serviceregistrar", false, false, 0, nil, + {createTestServiceQuest(), string(createTestServiceRecordListFormWithDetails()), false, false, 0, nil, string(createTestServiceRecordListFormWithDetails()), false, "Good case, everything passes with one service details"}, - {createTestServiceQuest(), string(createTestServiceRecordListForm()), - "http://localhost:20102/serviceregistrar", false, false, 2, errHTTP, + {createTestServiceQuest(), string(createTestServiceRecordListForm()), false, false, 2, errHTTP, "", true, "Bad case, DefaultClient.Do fails"}, - {createTestServiceQuest(), string(createTestServiceRecordListForm()), - "http://localhost:20102/serviceregistrar", false, true, 0, nil, + {createTestServiceQuest(), string(createTestServiceRecordListForm()), false, true, 0, nil, "", true, "Bad case, ReadAll fails"}, - {createTestServiceQuest(), "hej hej", - "http://localhost:20102/serviceregistrar", false, false, 0, nil, + {createTestServiceQuest(), "hej hej", false, false, 0, nil, "", true, "Bad case, Unpack fails"}, - {createTestServiceQuest(), string(createTestServicePointForm()), - "http://localhost:20102/serviceregistrar", false, false, 0, nil, + {createTestServiceQuest(), string(createTestServicePointForm()), false, false, 0, nil, "", false, "Bad case, type assertion fails"}, - {createTestServiceQuest(), string(createEmptyServiceRecordListForm()), - "http://localhost:20102/serviceregistrar", false, false, 0, nil, + {createTestServiceQuest(), string(createEmptyServiceRecordListForm()), false, false, 0, nil, "", true, "Bad case, the service record list is empty"}, } func TestGetServicesURL(t *testing.T) { for _, testCase := range getServicesURLTestParams { - mua := createUnitAsset(testCase.leadingRegistrarUrl) + mua := createUnitAsset() if mua == nil { t.Fatalf("UAssets[\"Orchestration\"] is nil") } From 93ea24b77868dd74788379324a3838bfe93a3003 Mon Sep 17 00:00:00 2001 From: gabaxh Date: Fri, 18 Jul 2025 08:52:32 +0200 Subject: [PATCH 26/81] Fixed some missed spellchecks --- orchestrator/thing_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/orchestrator/thing_test.go b/orchestrator/thing_test.go index 3b58d65..b53e591 100644 --- a/orchestrator/thing_test.go +++ b/orchestrator/thing_test.go @@ -228,10 +228,10 @@ func createTestServiceRecordListFormWithDefinition() []byte { serviceRecordFormWithDefinition.IPAddresses = []string{"123.456.789"} serviceRecordFormWithDefinition.ProtoPort = map[string]int{"http": 123} serviceRecordFormWithDefinition.ServiceDefinition = "temperature" - var serviceRecordListFormWithDefintion forms.ServiceRecordList_v1 - serviceRecordListFormWithDefintion.NewForm() - serviceRecordListFormWithDefintion.List = []forms.ServiceRecord_v1{serviceRecordFormWithDefinition} - fakebody, err := json.MarshalIndent(serviceRecordListFormWithDefintion, "", " ") + var serviceRecordListFormWithDefinition forms.ServiceRecordList_v1 + serviceRecordListFormWithDefinition.NewForm() + serviceRecordListFormWithDefinition.List = []forms.ServiceRecord_v1{serviceRecordFormWithDefinition} + fakebody, err := json.MarshalIndent(serviceRecordListFormWithDefinition, "", " ") if err != nil { log.Fatalf("Fail marshal at start of test: %v", err) } From cc923e07faa8f95a2b47854f361b9be39a398bc6 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 18 Jul 2025 11:16:45 +0200 Subject: [PATCH 27/81] Adds go.mod files as they are required for Go projects --- .gitignore | 2 -- orchestrator/go.mod | 8 ++++++++ orchestrator/go.sum | 2 ++ 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 orchestrator/go.mod create mode 100644 orchestrator/go.sum diff --git a/.gitignore b/.gitignore index 9d2254d..8ca2c9f 100644 --- a/.gitignore +++ b/.gitignore @@ -25,8 +25,6 @@ vendor/ # mbaigo *.json -go.mod -go.sum serviceRegistry.db *.pem **/files/ diff --git a/orchestrator/go.mod b/orchestrator/go.mod new file mode 100644 index 0000000..85bde11 --- /dev/null +++ b/orchestrator/go.mod @@ -0,0 +1,8 @@ +module github.com/sdoque/systems/orchestrator + +go 1.24.4 + +require github.com/sdoque/mbaigo v0.0.0-20250520155324-7390c339652a + +// Replaces this library with a patched version +replace github.com/sdoque/mbaigo v0.0.0-20250520155324-7390c339652a => github.com/lmas/mbaigo v0.0.0-20250715100940-0fef178d190b diff --git a/orchestrator/go.sum b/orchestrator/go.sum new file mode 100644 index 0000000..ef3db4b --- /dev/null +++ b/orchestrator/go.sum @@ -0,0 +1,2 @@ +github.com/lmas/mbaigo v0.0.0-20250715100940-0fef178d190b h1:4I+X0TTj6E2RvaAIG8EV7TzZ9O4oPOOBJiWR/otOOJg= +github.com/lmas/mbaigo v0.0.0-20250715100940-0fef178d190b/go.mod h1:vXE1mDd88Tap9bHm1elrk3Ht8bcImA3FeiSM03yUwsM= From 5d16ec2549469250f03f3b622ffbffa726806043 Mon Sep 17 00:00:00 2001 From: gabaxh Date: Fri, 18 Jul 2025 12:16:00 +0200 Subject: [PATCH 28/81] Fixed some PR comments --- orchestrator/extra_utils_test.go | 24 +++++--------- orchestrator/thing.go | 56 ++++++++++++++++---------------- 2 files changed, 37 insertions(+), 43 deletions(-) diff --git a/orchestrator/extra_utils_test.go b/orchestrator/extra_utils_test.go index 8351788..5523ccc 100644 --- a/orchestrator/extra_utils_test.go +++ b/orchestrator/extra_utils_test.go @@ -63,15 +63,11 @@ func createUnitAsset() *UnitAsset { Description: "looks for the desired service described in a quest form (POST)", } - assetTraits := Traits{ - leadingRegistrar: "", - } - // create the unit asset template uat := &UnitAsset{ - Name: "orchestration", - Details: map[string][]string{"Platform": {"Independent"}}, - Traits: assetTraits, + Name: "orchestration", + Details: map[string][]string{"Platform": {"Independent"}}, + leadingRegistrar: "", ServicesMap: components.Services{ squest.SubPath: &squest, // Inline assignment of the temperature service }, @@ -225,15 +221,13 @@ func createTestSystem(broken bool) (sys components.System) { ServicesMap := &components.Services{ setTest.SubPath: setTest, } - assetTraits := Traits{ - leadingRegistrar: "", - } + mua := &UnitAsset{ - Name: "testUnitAsset", - Details: map[string][]string{"Test": {"Test"}}, - Traits: assetTraits, - ServicesMap: *ServicesMap, - CervicesMap: *CervicesMap, + Name: "testUnitAsset", + Details: map[string][]string{"Test": {"Test"}}, + leadingRegistrar: "", + ServicesMap: *ServicesMap, + CervicesMap: *CervicesMap, } sys.UAssets = make(map[string]*components.UnitAsset) diff --git a/orchestrator/thing.go b/orchestrator/thing.go index e2c66b4..867afe3 100644 --- a/orchestrator/thing.go +++ b/orchestrator/thing.go @@ -33,9 +33,9 @@ import ( //-------------------------------------Define the Thing's resource // Traits are Asset-specific configurable parameters and variables -type Traits struct { - leadingRegistrar string -} +// type Traits struct { +// leadingRegistrar string +// } // UnitAsset type models the unit asset (interface) of the system. type UnitAsset struct { @@ -45,7 +45,8 @@ type UnitAsset struct { ServicesMap components.Services `json:"-"` CervicesMap components.Cervices `json:"-"` // - Traits + leadingRegistrar string + // Traits } // GetName returns the name of the Resource. @@ -69,9 +70,11 @@ func (ua *UnitAsset) GetDetails() map[string][]string { } // GetTraits returns the traits of the Resource. +/* func (ua *UnitAsset) GetTraits() any { return ua.Traits } +*/ // ensure UnitAsset implements components.UnitAsset (this check is done at during the compilation) var _ components.UnitAsset = (*UnitAsset)(nil) @@ -94,15 +97,18 @@ func initTemplate() components.UnitAsset { Description: "looks for the desired services described in a quest form (POST)", } - assetTraits := Traits{ - leadingRegistrar: "", // Initialize the leading registrar to nil - } + /* + assetTraits := Traits{ + leadingRegistrar: "", // Initialize the leading registrar to nil + } + */ // create the unit asset template uat := &UnitAsset{ - Name: "orchestration", - Details: map[string][]string{"Platform": {"Independent"}}, - Traits: assetTraits, + Name: "orchestration", + Details: map[string][]string{"Platform": {"Independent"}}, + leadingRegistrar: "", + //Traits: assetTraits, ServicesMap: components.Services{ squest.SubPath: &squest, // Inline assignment of the temperature service squests.SubPath: &squests, @@ -123,12 +129,14 @@ func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.Sys ServicesMap: usecases.MakeServiceMap(configuredAsset.Services), } - traits, err := UnmarshalTraits(configuredAsset.Traits) - if err != nil { - log.Println("Warning: could not unmarshal traits:", err) - } else if len(traits) > 0 { - ua.Traits = traits[0] // or handle multiple traits if needed - } + /* + traits, err := UnmarshalTraits(configuredAsset.Traits) + if err != nil { + log.Println("Warning: could not unmarshal traits:", err) + } else if len(traits) > 0 { + ua.Traits = traits[0] // or handle multiple traits if needed + } + */ // start the unit asset(s) // no need to start the algorithm asset @@ -138,6 +146,7 @@ func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.Sys } } +/* // UnmarshalTraits unmarshals a slice of json.RawMessage into a slice of Traits. func UnmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { var traitsList []Traits @@ -150,6 +159,7 @@ func UnmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { } return traitsList, nil } +*/ //-------------------------------------Thing's resource functions @@ -165,8 +175,7 @@ func UnmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { // - servLoc: A byte slice containing the service location in JSON format. // - err: An error if any issues occur during the process. func (ua *UnitAsset) getServiceURL(newQuest forms.ServiceQuest_v1) (servLoc []byte, err error) { - // ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) // Create a new context, with a 2-second timeout - ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second) // Create a new context, with a 2-second timeout + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) // Create a new context, with a 2-second timeout defer cancel() sys := ua.Owner if ua.leadingRegistrar == "" { @@ -195,7 +204,6 @@ func (ua *UnitAsset) getServiceURL(newQuest forms.ServiceQuest_v1) (servLoc []by req = req.WithContext(ctx) // associate the cancellable context with the request // forward the request to the leading Service Registrar///////////////////////////////// - // client := &http.Client{} resp, err := http.DefaultClient.Do(req) if err != nil { ua.leadingRegistrar = "" @@ -207,7 +215,6 @@ func (ua *UnitAsset) getServiceURL(newQuest forms.ServiceQuest_v1) (servLoc []by log.Printf("Error reading discovery response body: %v", err) return servLoc, err } - fmt.Printf("\n%v\n", string(respBytes)) serviceListf, err := usecases.Unpack(respBytes, mediaType) if err != nil { log.Print("Error extracting discovery reply ", err) @@ -226,10 +233,8 @@ func (ua *UnitAsset) getServiceURL(newQuest forms.ServiceQuest_v1) (servLoc []by return } - fmt.Printf("/n the length of the service list is: %d\n", len(serviceList.List)) serviceLocation := selectService(*serviceList) payload, err := json.MarshalIndent(serviceLocation, "", " ") - fmt.Printf("the service location is %+v\n", serviceLocation) return payload, err } @@ -245,8 +250,7 @@ func selectService(serviceList forms.ServiceRecordList_v1) (sp forms.ServicePoin } func (ua *UnitAsset) getServicesURL(newQuest forms.ServiceQuest_v1) (servLoc []byte, err error) { - // ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) // Create a new context, with a 2-second timeout - ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second) // Create a new context, with a 2-second timeout + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) // Create a new context, with a 2-second timeout defer cancel() sys := ua.Owner if ua.leadingRegistrar == "" { @@ -275,7 +279,6 @@ func (ua *UnitAsset) getServicesURL(newQuest forms.ServiceQuest_v1) (servLoc []b req = req.WithContext(ctx) // associate the cancellable context with the request // forward the request to the leading Service Registrar///////////////////////////////// - // client := &http.Client{} resp, err := http.DefaultClient.Do(req) if err != nil { ua.leadingRegistrar = "" @@ -287,7 +290,6 @@ func (ua *UnitAsset) getServicesURL(newQuest forms.ServiceQuest_v1) (servLoc []b log.Printf("Error reading discovery response body: %v", err) return servLoc, err } - fmt.Printf("\n%v\n", string(respBytes)) serviceListf, err := usecases.Unpack(respBytes, mediaType) if err != nil { log.Print("Error extracting discovery reply ", err) @@ -306,8 +308,6 @@ func (ua *UnitAsset) getServicesURL(newQuest forms.ServiceQuest_v1) (servLoc []b return } - fmt.Printf("/n the length of the service list is: %d\n", len(serviceList.List)) payload, err := json.MarshalIndent(serviceList, "", " ") - fmt.Printf("the service location is %+v\n", serviceList) return payload, err } From 203438e75500c3abaa9fce52f467fc6c15b53273 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 18 Jul 2025 13:38:50 +0200 Subject: [PATCH 29/81] Removes old and misleading comments --- orchestrator/extra_utils_test.go | 50 ------------------ orchestrator/orchestrator.go | 15 +++--- orchestrator/thing.go | 87 ++++++-------------------------- 3 files changed, 23 insertions(+), 129 deletions(-) diff --git a/orchestrator/extra_utils_test.go b/orchestrator/extra_utils_test.go index 5523ccc..fdc56be 100644 --- a/orchestrator/extra_utils_test.go +++ b/orchestrator/extra_utils_test.go @@ -79,56 +79,6 @@ func createUnitAsset() *UnitAsset { return uat } -/* - -// A mocked UnitAsset used for testing -type mockUnitAsset struct { - Name string `json:"name"` // Must be a unique name, ie. a sensor ID - Owner *components.System `json:"-"` // The parent system this UA is part of - Details map[string][]string `json:"details"` // Metadata or details about this UA - ServicesMap components.Services `json:"-"` - CervicesMap components.Cervices `json:"-"` -} - -func (mua mockUnitAsset) GetName() string { - return mua.Name -} - -func (mua mockUnitAsset) GetServices() components.Services { - return mua.ServicesMap -} - -func (mua mockUnitAsset) GetCervices() components.Cervices { - return mua.CervicesMap -} - -func (mua mockUnitAsset) GetDetails() map[string][]string { - return mua.Details -} - -func (mua mockUnitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath string) {} - -// A mocked form used for testing -type mockForm struct { - XMLName xml.Name `json:"-" xml:"testName"` - Value any `json:"value" xml:"value"` - Unit string `json:"unit" xml:"unit"` - Version string `json:"version" xml:"version"` -} - -// NewForm creates a new form -func (f mockForm) NewForm() forms.Form { - f.Version = "testVersion" - return f -} - -// FormVersion returns the version of the form -func (f mockForm) FormVersion() string { - return f.Version -} - -*/ - type errorReader struct{} func (errorReader) Read(p []byte) (int, error) { diff --git a/orchestrator/orchestrator.go b/orchestrator/orchestrator.go index 6b8f010..da3a4dc 100644 --- a/orchestrator/orchestrator.go +++ b/orchestrator/orchestrator.go @@ -31,8 +31,8 @@ import ( func main() { // prepare for graceful shutdown - ctx, cancel := context.WithCancel(context.Background()) // create a context that can be cancelled - defer cancel() // make sure all paths cancel the context to avoid context leak + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() // instantiate the System sys := components.NewSystem("orchestrator", ctx) @@ -87,8 +87,9 @@ func main() { // wait for shutdown signal, and gracefully close properly goroutines with context <-sys.Sigs // wait for a SIGINT (Ctrl+C) signal fmt.Println("\nshuting down system", sys.Name) - cancel() // cancel the context, signaling the goroutines to stop - time.Sleep(2 * time.Second) // allow the go routines to be executed, which might take more time than the main routine to end + cancel() // signal the goroutines to stop + // allow the go routines to be executed, which might take more time than the main routine to end + time.Sleep(2 * time.Second) } // Serving handles the resources services. NOTE: it expects those names from the request URL path @@ -115,7 +116,7 @@ func (ua *UnitAsset) orchestrate(w http.ResponseWriter, r *http.Request) { } defer r.Body.Close() - bodyBytes, err := io.ReadAll(r.Body) // Use io.ReadAll instead of ioutil.ReadAll + bodyBytes, err := io.ReadAll(r.Body) if err != nil { log.Printf("error reading discovery request body: %v\n", err) return @@ -125,7 +126,6 @@ func (ua *UnitAsset) orchestrate(w http.ResponseWriter, r *http.Request) { if err != nil { log.Printf("error extracting the discovery request %v\n", err) } - // Perform a type assertion to convert the returned Form to SignalA_v1a qf, ok := questForm.(*forms.ServiceQuest_v1) if !ok { fmt.Println("Problem unpacking the service discovery request form") @@ -162,7 +162,7 @@ func (ua *UnitAsset) orchestrateMultiple(w http.ResponseWriter, r *http.Request) } defer r.Body.Close() - bodyBytes, err := io.ReadAll(r.Body) // Use io.ReadAll instead of ioutil.ReadAll + bodyBytes, err := io.ReadAll(r.Body) if err != nil { log.Printf("error reading discovery request body: %v\n", err) return @@ -172,7 +172,6 @@ func (ua *UnitAsset) orchestrateMultiple(w http.ResponseWriter, r *http.Request) if err != nil { log.Printf("error extracting the discovery request %v\n", err) } - // Perform a type assertion to convert the returned Form to SignalA_v1a qf, ok := questForm.(*forms.ServiceQuest_v1) if !ok { fmt.Println("Problem unpacking the service discovery request form") diff --git a/orchestrator/thing.go b/orchestrator/thing.go index 867afe3..d93cc24 100644 --- a/orchestrator/thing.go +++ b/orchestrator/thing.go @@ -32,21 +32,14 @@ import ( //-------------------------------------Define the Thing's resource -// Traits are Asset-specific configurable parameters and variables -// type Traits struct { -// leadingRegistrar string -// } - // UnitAsset type models the unit asset (interface) of the system. type UnitAsset struct { - Name string `json:"name"` - Owner *components.System `json:"-"` - Details map[string][]string `json:"details"` - ServicesMap components.Services `json:"-"` - CervicesMap components.Cervices `json:"-"` - // + Name string `json:"name"` + Owner *components.System `json:"-"` + Details map[string][]string `json:"details"` + ServicesMap components.Services `json:"-"` + CervicesMap components.Cervices `json:"-"` leadingRegistrar string - // Traits } // GetName returns the name of the Resource. @@ -69,13 +62,6 @@ func (ua *UnitAsset) GetDetails() map[string][]string { return ua.Details } -// GetTraits returns the traits of the Resource. -/* -func (ua *UnitAsset) GetTraits() any { - return ua.Traits -} -*/ - // ensure UnitAsset implements components.UnitAsset (this check is done at during the compilation) var _ components.UnitAsset = (*UnitAsset)(nil) @@ -97,20 +83,13 @@ func initTemplate() components.UnitAsset { Description: "looks for the desired services described in a quest form (POST)", } - /* - assetTraits := Traits{ - leadingRegistrar: "", // Initialize the leading registrar to nil - } - */ - // create the unit asset template uat := &UnitAsset{ Name: "orchestration", Details: map[string][]string{"Platform": {"Independent"}}, leadingRegistrar: "", - //Traits: assetTraits, ServicesMap: components.Services{ - squest.SubPath: &squest, // Inline assignment of the temperature service + squest.SubPath: &squest, squests.SubPath: &squests, }, } @@ -121,7 +100,6 @@ func initTemplate() components.UnitAsset { // newResource creates the Resource resource with its pointers and channels based on the configuration using the template func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.System) (components.UnitAsset, func()) { - // var ua components.UnitAsset // this is an interface, which we then initialize ua := &UnitAsset{ // this is an interface, which we then initialize Name: configuredAsset.Name, Owner: sys, @@ -129,44 +107,19 @@ func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.Sys ServicesMap: usecases.MakeServiceMap(configuredAsset.Services), } - /* - traits, err := UnmarshalTraits(configuredAsset.Traits) - if err != nil { - log.Println("Warning: could not unmarshal traits:", err) - } else if len(traits) > 0 { - ua.Traits = traits[0] // or handle multiple traits if needed - } - */ - - // start the unit asset(s) - // no need to start the algorithm asset - return ua, func() { log.Println("Ending orchestration services") } } -/* -// UnmarshalTraits unmarshals a slice of json.RawMessage into a slice of Traits. -func UnmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { - var traitsList []Traits - for _, raw := range rawTraits { - var t Traits - if err := json.Unmarshal(raw, &t); err != nil { - return nil, fmt.Errorf("failed to unmarshal trait: %w", err) - } - traitsList = append(traitsList, t) - } - return traitsList, nil -} -*/ - //-------------------------------------Thing's resource functions // getServiceURL retrieves the service URL for a given ServiceQuest_v1. // It first checks if the leading registrar is still valid and updates it if necessary. -// If no leading registrar is found, it iterates through the system's core services to find one. -// Once a valid registrar is found, it sends a query to the registrar to get the service URL. +// If no leading registrar is found, it iterates through the system's core services +// to find one. +// Once a valid registrar is found, it sends a query to the registrar to get the +// service URL. // // Parameters: // - newQuest: The ServiceQuest_v1 containing the service request details. @@ -175,7 +128,7 @@ func UnmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { // - servLoc: A byte slice containing the service location in JSON format. // - err: An error if any issues occur during the process. func (ua *UnitAsset) getServiceURL(newQuest forms.ServiceQuest_v1) (servLoc []byte, err error) { - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) // Create a new context, with a 2-second timeout + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() sys := ua.Owner if ua.leadingRegistrar == "" { @@ -186,8 +139,6 @@ func (ua *UnitAsset) getServiceURL(newQuest forms.ServiceQuest_v1) (servLoc []by } // Create a new HTTP request to the the Service Registrar - - // Create buffer to save a copy of the request body mediaType := "application/json" jsonQF, err := usecases.Pack(&newQuest, mediaType) if err != nil { @@ -200,10 +151,9 @@ func (ua *UnitAsset) getServiceURL(newQuest forms.ServiceQuest_v1) (servLoc []by if err != nil { return servLoc, err } - req.Header.Set("Content-Type", mediaType) // set the Content-Type header - req = req.WithContext(ctx) // associate the cancellable context with the request + req.Header.Set("Content-Type", mediaType) + req = req.WithContext(ctx) - // forward the request to the leading Service Registrar///////////////////////////////// resp, err := http.DefaultClient.Do(req) if err != nil { ua.leadingRegistrar = "" @@ -221,7 +171,6 @@ func (ua *UnitAsset) getServiceURL(newQuest forms.ServiceQuest_v1) (servLoc []by return servLoc, err } - // Perform a type assertion to convert the returned Form to SignalA_v1a serviceList, ok := serviceListf.(*forms.ServiceRecordList_v1) if !ok { log.Println("problem asserting the type of the service list form") @@ -250,7 +199,7 @@ func selectService(serviceList forms.ServiceRecordList_v1) (sp forms.ServicePoin } func (ua *UnitAsset) getServicesURL(newQuest forms.ServiceQuest_v1) (servLoc []byte, err error) { - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) // Create a new context, with a 2-second timeout + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() sys := ua.Owner if ua.leadingRegistrar == "" { @@ -261,8 +210,6 @@ func (ua *UnitAsset) getServicesURL(newQuest forms.ServiceQuest_v1) (servLoc []b } // Create a new HTTP request to the the Service Registrar - - // Create buffer to save a copy of the request body mediaType := "application/json" jsonQF, err := usecases.Pack(&newQuest, mediaType) if err != nil { @@ -275,10 +222,9 @@ func (ua *UnitAsset) getServicesURL(newQuest forms.ServiceQuest_v1) (servLoc []b if err != nil { return servLoc, err } - req.Header.Set("Content-Type", mediaType) // set the Content-Type header - req = req.WithContext(ctx) // associate the cancellable context with the request + req.Header.Set("Content-Type", mediaType) + req = req.WithContext(ctx) - // forward the request to the leading Service Registrar///////////////////////////////// resp, err := http.DefaultClient.Do(req) if err != nil { ua.leadingRegistrar = "" @@ -296,7 +242,6 @@ func (ua *UnitAsset) getServicesURL(newQuest forms.ServiceQuest_v1) (servLoc []b return servLoc, err } - // Perform a type assertion to convert the returned Form to SignalA_v1a serviceList, ok := serviceListf.(*forms.ServiceRecordList_v1) if !ok { log.Println("problem asserting the type of the service list form") From f2de22d3ce9aa6a15a1113dfd6c1dfb5c8f81b92 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 18 Jul 2025 13:50:05 +0200 Subject: [PATCH 30/81] Removes spammy prints and the rest were made uniform, fixes tests/linter warnings --- orchestrator/orchestrator.go | 11 +++++------ orchestrator/thing.go | 21 +++++---------------- orchestrator/thing_test.go | 4 ++-- 3 files changed, 12 insertions(+), 24 deletions(-) diff --git a/orchestrator/orchestrator.go b/orchestrator/orchestrator.go index da3a4dc..8c34177 100644 --- a/orchestrator/orchestrator.go +++ b/orchestrator/orchestrator.go @@ -17,7 +17,6 @@ import ( "context" "crypto/x509/pkix" "encoding/json" - "fmt" "io" "log" "mime" @@ -86,7 +85,7 @@ func main() { // wait for shutdown signal, and gracefully close properly goroutines with context <-sys.Sigs // wait for a SIGINT (Ctrl+C) signal - fmt.Println("\nshuting down system", sys.Name) + log.Println("shutting down system", sys.Name) cancel() // signal the goroutines to stop // allow the go routines to be executed, which might take more time than the main routine to end time.Sleep(2 * time.Second) @@ -111,7 +110,7 @@ func (ua *UnitAsset) orchestrate(w http.ResponseWriter, r *http.Request) { contentType := r.Header.Get("Content-Type") mediaType, _, err := mime.ParseMediaType(contentType) if err != nil { - fmt.Println("Error parsing media type:", err) + log.Println("Error parsing media type:", err) return } @@ -128,7 +127,7 @@ func (ua *UnitAsset) orchestrate(w http.ResponseWriter, r *http.Request) { } qf, ok := questForm.(*forms.ServiceQuest_v1) if !ok { - fmt.Println("Problem unpacking the service discovery request form") + log.Println("Problem unpacking the service discovery request form") return } @@ -157,7 +156,7 @@ func (ua *UnitAsset) orchestrateMultiple(w http.ResponseWriter, r *http.Request) contentType := r.Header.Get("Content-Type") mediaType, _, err := mime.ParseMediaType(contentType) if err != nil { - fmt.Println("Error parsing media type:", err) + log.Println("Error parsing media type:", err) return } @@ -174,7 +173,7 @@ func (ua *UnitAsset) orchestrateMultiple(w http.ResponseWriter, r *http.Request) } qf, ok := questForm.(*forms.ServiceQuest_v1) if !ok { - fmt.Println("Problem unpacking the service discovery request form") + log.Println("Problem unpacking the service discovery request form") return } diff --git a/orchestrator/thing.go b/orchestrator/thing.go index d93cc24..23eee2b 100644 --- a/orchestrator/thing.go +++ b/orchestrator/thing.go @@ -19,7 +19,6 @@ import ( "encoding/json" "fmt" "io" - "log" "net/http" "strconv" @@ -108,7 +107,7 @@ func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.Sys } return ua, func() { - log.Println("Ending orchestration services") + // Do nothing } } @@ -142,7 +141,6 @@ func (ua *UnitAsset) getServiceURL(newQuest forms.ServiceQuest_v1) (servLoc []by mediaType := "application/json" jsonQF, err := usecases.Pack(&newQuest, mediaType) if err != nil { - log.Printf("problem encountered when marshalling the service quest\n") return servLoc, err } @@ -162,24 +160,20 @@ func (ua *UnitAsset) getServiceURL(newQuest forms.ServiceQuest_v1) (servLoc []by defer resp.Body.Close() respBytes, err := io.ReadAll(resp.Body) if err != nil { - log.Printf("Error reading discovery response body: %v", err) return servLoc, err } serviceListf, err := usecases.Unpack(respBytes, mediaType) if err != nil { - log.Print("Error extracting discovery reply ", err) return servLoc, err } serviceList, ok := serviceListf.(*forms.ServiceRecordList_v1) if !ok { - log.Println("problem asserting the type of the service list form") - return + return nil, fmt.Errorf("problem asserting the type of the service list form") } if len(serviceList.List) == 0 { - err = fmt.Errorf("unable to locate any such service: %s", newQuest.ServiceDefinition) - return + return nil, fmt.Errorf("unable to locate any such service: %s", newQuest.ServiceDefinition) } serviceLocation := selectService(*serviceList) @@ -213,7 +207,6 @@ func (ua *UnitAsset) getServicesURL(newQuest forms.ServiceQuest_v1) (servLoc []b mediaType := "application/json" jsonQF, err := usecases.Pack(&newQuest, mediaType) if err != nil { - log.Printf("problem encountered when marshalling the service quest\n") return servLoc, err } @@ -233,24 +226,20 @@ func (ua *UnitAsset) getServicesURL(newQuest forms.ServiceQuest_v1) (servLoc []b defer resp.Body.Close() respBytes, err := io.ReadAll(resp.Body) if err != nil { - log.Printf("Error reading discovery response body: %v", err) return servLoc, err } serviceListf, err := usecases.Unpack(respBytes, mediaType) if err != nil { - log.Print("Error extracting discovery reply ", err) return servLoc, err } serviceList, ok := serviceListf.(*forms.ServiceRecordList_v1) if !ok { - log.Println("problem asserting the type of the service list form") - return + return nil, fmt.Errorf("problem asserting the type of the service list form") } if len(serviceList.List) == 0 { - err = fmt.Errorf("unable to locate any such service: %s", newQuest.ServiceDefinition) - return + return nil, fmt.Errorf("unable to locate any such service: %s", newQuest.ServiceDefinition) } payload, err := json.MarshalIndent(serviceList, "", " ") diff --git a/orchestrator/thing_test.go b/orchestrator/thing_test.go index b53e591..946e3cb 100644 --- a/orchestrator/thing_test.go +++ b/orchestrator/thing_test.go @@ -149,7 +149,7 @@ var getServiceURLTestParams = []getServiceURLTestStruct{ {createTestServiceQuest(), "hej hej", false, false, 0, nil, "", true, "Bad case, Unpack fails"}, {createTestServiceQuest(), string(createTestServicePointForm()), false, false, - 0, nil, "", false, "Bad case, type assertion fails"}, + 0, nil, "", true, "Bad case, type assertion fails"}, {createTestServiceQuest(), string(createEmptyServiceRecordListForm()), false, false, 0, nil, "", true, "Bad case, the service record list is empty"}, } @@ -286,7 +286,7 @@ var getServicesURLTestParams = []getServicesURLTestStruct{ "", true, "Bad case, Unpack fails"}, {createTestServiceQuest(), string(createTestServicePointForm()), false, false, 0, nil, - "", false, + "", true, "Bad case, type assertion fails"}, {createTestServiceQuest(), string(createEmptyServiceRecordListForm()), false, false, 0, nil, "", true, From b0d7d033ddaf98541161d6bc1dcae7308550b3e3 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 18 Jul 2025 14:02:43 +0200 Subject: [PATCH 31/81] Removes commented out integration test, it will be handled separately --- orchestrator/integration_test.go | 413 ------------------------------- 1 file changed, 413 deletions(-) delete mode 100644 orchestrator/integration_test.go diff --git a/orchestrator/integration_test.go b/orchestrator/integration_test.go deleted file mode 100644 index ebd793f..0000000 --- a/orchestrator/integration_test.go +++ /dev/null @@ -1,413 +0,0 @@ -package main - -/* - -type requestEvent struct { - event string - hits int - body []byte -} - -// Mock simulating traffic between a system and registrars/orchestrators -type mockTrans struct { - t *testing.T - hits map[string]int // Used to track http requests - mutex sync.Mutex // For protecting access to the above map - events chan requestEvent // Tracks service "events" and requests to the cloud services -} - -func newIntegrationMockTransport(t *testing.T) *mockTrans { - m := &mockTrans{ - t: t, - hits: make(map[string]int), - events: make(chan requestEvent), - } - // Hijack the default http client so no actual http requests are sent over the network - http.DefaultClient.Transport = m - return m -} - -func (m *mockTrans) waitFor(event string) (int, []byte, error) { - select { - case e := <-m.events: - if e.event != event { - return 0, nil, fmt.Errorf("got %s, expected %s", e.event, event) - } - return e.hits, e.body, nil - case <-time.Tick(10 * time.Second): - return 0, nil, fmt.Errorf("event timeout") - } -} - -func newServiceRecord() []byte { - f := forms.ServiceRecord_v1{ - Id: 13, // NOTE: this should match with eventUnregister - Created: time.Now().Format(time.RFC3339), - EndOfValidity: time.Now().Format(time.RFC3339), - Version: "ServiceRecord_v1", - } - b, err := usecases.Pack(&f, "application/json") - if err != nil { - panic(err) // Hard fail if Pack() can't handle the above form - } - return b -} - -func newServicePoint() []byte { - f := forms.ServicePoint_v1{ - // per usecases/registration.go:serviceRegistrationForm() - ServNode: fmt.Sprintf("localhost_%s_%s_%s", systemName, unitName, unitService), - // per orchestrator/thing.go:selectService() - ServLocation: fmt.Sprintf("http://localhost:%d/%s/%s/%s", - systemPort, systemName, unitName, unitService, - ), - Version: "ServicePoint_v1", - } - b, err := usecases.Pack(&f, "application/json") - if err != nil { - panic(err) // Another hard fail if Pack() can't work with the above form - } - return b -} - -const ( - eventRegistryStatus string = "GET /serviceregistrar/registry/status" - eventRegister string = "POST /serviceregistrar/registry/register" - eventUnregister string = "DELETE /serviceregistrar/registry/unregister/13" - eventOrchestration string = "GET /orchestrator/orchestration" - eventOrchestrate string = "POST /orchestrator/orchestration/squest" -) - -var mockRequests = map[string]struct { - sendEvent bool - status int - body []byte -}{ - eventRegistryStatus: {false, 200, []byte(components.ServiceRegistrarLeader)}, - eventRegister: {true, 200, newServiceRecord()}, - eventUnregister: {true, 200, nil}, - eventOrchestration: {false, 200, nil}, - eventOrchestrate: {true, 200, newServicePoint()}, -} - -func (m *mockTrans) RoundTrip(req *http.Request) (*http.Response, error) { - m.mutex.Lock() // This lock is mainly for guarding concurrent access to the hits map - defer m.mutex.Unlock() - event := req.Method + " " + req.URL.Path - m.hits[event] += 1 - if event == serviceURL { - // The example service will, through the system, return a proper response - return http.DefaultTransport.RoundTrip(req) - } - - // Any other requests needs to be mocked, simulating responses from the - // service registrar and orchestrator. - mock, found := mockRequests[event] - if !found { - m.t.Errorf("unknown request: %s", event) - // Let's see how the system responds to this - mock.status = http.StatusNotImplemented - mock.body = []byte(http.StatusText(mock.status)) - } - rec := httptest.NewRecorder() - rec.Header().Set("Content-Type", "application/json") - rec.WriteHeader(mock.status) - rec.Write(mock.body) // Safe to ignore the returned error, it's always nil - - // Allows for syncing up the test, with the request flow performed by the system - if mock.sendEvent { - var b []byte - if req.Body != nil { - var err error - b, err = io.ReadAll(req.Body) - if err != nil { - m.t.Errorf("failed reading request body: %v", err) - } - defer req.Body.Close() - } - // Using a goroutine prevents thread locking - go func(e string, h int, b []byte) { - m.events <- requestEvent{e, h, b} - }(event, m.hits[event], b) - } - return rec.Result(), nil -} - -//////////////////////////////////////////////////////////////////////////////// - -func countGoroutines() (int, string) { - c := runtime.NumGoroutine() - buf := &bytes.Buffer{} - // A write to this buffer will always return nil error, so safe to ignore here. - // This call will spawn some goroutine too, so need to chill for a little while. - _ = pprof.Lookup("goroutine").WriteTo(buf, 2) - trace := buf.String() - // Calling signal.Notify() will leave an extra goroutine that runs forever, - // so it should be subtracted from the count. For more info, see: - // https://github.com/golang/go/issues/52619 - // https://github.com/golang/go/issues/72803 - // https://github.com/golang/go/issues/21576 - if strings.Contains(trace, "os/signal.signal_recv") { - c -= 1 - } - return c, trace -} - -func assertNotEq(t *testing.T, got, want any) { - if got != want { - t.Errorf("got %v, expected %v", got, want) - } -} - -func TestSimpleSystemIntegration(t *testing.T) { - routinesStart, _ := countGoroutines() - m := newIntegrationMockTransport(t) - sys, stopSystem, err := newSystem() - if err != nil { - t.Fatalf("expected no error, got: %s", err) - } - - // Validate service registration - hits, body, err := m.waitFor(eventRegister) - assertNotEq(t, err, nil) - if hits != 1 { - t.Errorf("system skipped: %s", eventRegister) - } - var sr forms.ServiceRecord_v1 - err = json.Unmarshal(body, &sr) - assertNotEq(t, err, nil) - assertNotEq(t, sr.SystemName, systemName) - assertNotEq(t, sr.SubPath, path.Join(unitName, unitService)) - - // Validate service usage - ua := *sys.UAssets[unitName] - if ua == nil { - t.Fatalf("system missing unit asset: %s", unitName) - } - service := ua.GetCervices()[unitService] - if service == nil { - t.Fatalf("unit asset missing cervice: %s", unitService) - } - f, err := usecases.GetState(service, sys) - assertNotEq(t, err, nil) - fs, ok := f.(*forms.SignalA_v1a) - if ok == false || fs == nil || fs.Value == 0.0 { - t.Errorf("invalid form: %#v", f) - } - - // Late validation for service discovery - hits, body, err = m.waitFor(eventOrchestrate) - assertNotEq(t, err, nil) - if hits != 1 { - t.Errorf("system skipped: %s", eventUnregister) - } - var sq forms.ServiceQuest_v1 - err = json.Unmarshal(body, &sq) - assertNotEq(t, err, nil) - assertNotEq(t, sq.ServiceDefinition, unitService) - - // Validate service unregister - stopSystem() - hits, _, err = m.waitFor(eventUnregister) // NOTE: doesn't receive a body - assertNotEq(t, err, nil) - if hits != 1 { - t.Errorf("system skipped: %s", eventUnregister) - } - - // Detect any leaking goroutines - // Delay a short moment and let the goroutines finish. Not sure if there's - // a better way to wait for an _unknown number_ of goroutines. - // This might give flaky test results in slower environments! - time.Sleep(1 * time.Second) - routinesStop, trace := countGoroutines() - if (routinesStop - routinesStart) != 0 { - t.Errorf("leaking goroutines: count at start=%d, stop=%d\n%s", - routinesStart, routinesStop, trace, - ) - } -} - -const ( - unitName string = "randomiser" - unitService string = "random" -) - -// The most simplest unit asset -type uaRandomiser struct { - Name string `json:"-"` - Owner *components.System `json:"-"` - Details map[string][]string `json:"-"` - ServicesMap components.Services `json:"-"` - CervicesMap components.Cervices `json:"-"` -} - -// Force type check (fulfilling the interface) at compile time -var _ components.UnitAsset = &uaRandomiser{} - -// Add required functions to fulfil the UnitAsset interface -func (ua uaRandomiser) GetName() string { return ua.Name } -func (ua uaRandomiser) GetServices() components.Services { return ua.ServicesMap } -func (ua uaRandomiser) GetCervices() components.Cervices { return ua.CervicesMap } -func (ua uaRandomiser) GetDetails() map[string][]string { return ua.Details } - -func (ua uaRandomiser) Serving(w http.ResponseWriter, r *http.Request, servicePath string) { - if servicePath != unitService { - http.Error(w, "unknown service path: "+servicePath, http.StatusBadRequest) - return - } - - f := forms.SignalA_v1a{ - Value: rand.Float64(), - } - b, err := usecases.Pack(f.NewForm(), "application/json") - if err != nil { - http.Error(w, "error from Pack: "+err.Error(), http.StatusInternalServerError) - return - } - if _, err := w.Write(b); err != nil { - http.Error(w, "error from Write: "+err.Error(), http.StatusInternalServerError) - } -} - -func createUATemplate(sys *components.System) { - s := &components.Service{ - Definition: unitService, // The "name" of the service - SubPath: unitService, // Not "allowed" to be changed afterwards - Details: map[string][]string{"key1": {"value1"}}, - RegPeriod: 60, - // NOTE: must start with lower-case, it gets embedded into another sentence in the web API - Description: "returns a random float64", - } - ua := components.UnitAsset(&uaRandomiser{ - Name: unitName, // WARN: don't use the system name!! this is an asset! - Details: map[string][]string{"key2": {"value2"}}, - ServicesMap: components.Services{ - s.SubPath: s, - }, - }) - sys.UAssets[ua.GetName()] = &ua -} - -func loadUAConfig(ca usecases.ConfigurableAsset, sys *components.System) (components.UnitAsset, func()) { - s := ca.Services[0] - ua := &uaRandomiser{ - Name: ca.Name, - Owner: sys, - Details: ca.Details, - ServicesMap: usecases.MakeServiceMap(ca.Services), - // Let it consume its own service - CervicesMap: components.Cervices{unitService: &components.Cervice{ - Definition: s.Definition, - Details: s.Details, - // Nodes will be filled up by any discovered cervices - Nodes: make(map[string][]string, 0), - }}, - } - return ua, func() {} -} - -//////////////////////////////////////////////////////////////////////////////// - -const ( - systemName string = "test" - systemPort int = 29999 -) - -var serviceURL = "GET /" + path.Join(systemName, unitName, unitService) - -// The most simplest system -func newSystem() (*components.System, func(), error) { - ctx, cancel := context.WithCancel(context.Background()) - - // TODO: want this to return a pointer type instead! - // easier to use and pointer is used all the time anyway down below - sys := components.NewSystem(systemName, ctx) - sys.Husk = &components.Husk{ - Description: " is the most simplest system possible", - Details: map[string][]string{"key3": {"value3"}}, - ProtoPort: map[string]int{"http": systemPort}, - } - - // Setup default config with default unit asset and values - createUATemplate(&sys) - rawResources, err := usecases.Configure(&sys) - - // Extra check to work around "created config" error. Not required normally! - if err != nil { - // Return errors not related to config creation - if errors.Is(err, usecases.ErrNewConfig) == false { - cancel() - return nil, nil, err - } - // Since Configure() created the config file, it must be cleaned up when this test is done! - defer os.Remove("systemconfig.json") - // Default config file was created, redo the func call to load the file - rawResources, err = usecases.Configure(&sys) - if err != nil { - cancel() - return nil, nil, err - } - } - // NOTE: if the config file already existed (thus the above error block didn't - // get to run), then the config file should be left alone and not removed! - - // Load unit assets defined in the config file - cleanups, err := LoadResources(&sys, rawResources, loadUAConfig) - if err != nil { - cancel() - return nil, nil, err - } - - // TODO: this is not ready for production yet? - // usecases.RequestCertificate(&sys) - - usecases.RegisterServices(&sys) - - // TODO: prints logs - usecases.SetoutServers(&sys) - - stop := func() { - cancel() - // TODO: a waitgroup or something should be used to make sure all goroutines have stopped - // Not doing much in the mock cleanups so this works fine for now...? - cleanups() - } - return &sys, stop, nil -} - -type NewResourceFunc func(usecases.ConfigurableAsset, *components.System) (components.UnitAsset, func()) - -// LoadResources loads all unit assets from rawRes (which was loaded from "systemconfig.json" file) -// and calls newResFunc repeatedly for each loaded asset. -// The fully loaded unit asset and an optional cleanup function are collected from -// newResFunc and are then attached to the sys system. -// LoadResources then returns a system cleanup function and an optional error. -// The error always originate from [json.Unmarshal]. -func LoadResources(sys *components.System, rawRes []json.RawMessage, newResFunc NewResourceFunc) (func(), error) { - // Resets this map so it can be filled with loaded unit assets (rather than templates) - sys.UAssets = make(map[string]*components.UnitAsset) - - var cleanups []func() - for _, raw := range rawRes { - var ca usecases.ConfigurableAsset - if err := json.Unmarshal(raw, &ca); err != nil { - return func() {}, err - } - - ua, f := newResFunc(ca, sys) - sys.UAssets[ua.GetName()] = &ua - cleanups = append(cleanups, f) - } - - doCleanups := func() { - for _, f := range cleanups { - f() - } - // Stops hijacking SIGINT and return signal control to user - signal.Stop(sys.Sigs) - close(sys.Sigs) - } - return doCleanups, nil -} - -*/ From d1562f71ced4766cd54a1ea7a67694bf6927e7a2 Mon Sep 17 00:00:00 2001 From: gabaxh Date: Mon, 21 Jul 2025 13:47:43 +0200 Subject: [PATCH 32/81] Fixed PR comments --- orchestrator/extra_utils_test.go | 113 ++---------------------- orchestrator/orchestrator_test.go | 139 +++++++++++++++--------------- orchestrator/thing_test.go | 107 ++++------------------- 3 files changed, 94 insertions(+), 265 deletions(-) diff --git a/orchestrator/extra_utils_test.go b/orchestrator/extra_utils_test.go index fdc56be..67e728f 100644 --- a/orchestrator/extra_utils_test.go +++ b/orchestrator/extra_utils_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "net/http/httptest" "github.com/sdoque/mbaigo/components" ) @@ -86,9 +87,7 @@ func (errorReader) Read(p []byte) (int, error) { } type mockResponseWriter struct { - headers http.Header - body []byte - status int + *httptest.ResponseRecorder writeError bool } @@ -96,122 +95,24 @@ func (e *mockResponseWriter) Write(b []byte) (int, error) { if e.writeError { return 0, fmt.Errorf("Forced write error") } - e.body = append(e.body, b...) - return len(b), nil + return e.ResponseRecorder.Write(b) } func (e *mockResponseWriter) WriteHeader(statusCode int) { - e.status = statusCode + e.ResponseRecorder.Code = statusCode } func (e *mockResponseWriter) Header() http.Header { - return e.headers + return e.ResponseRecorder.Header() } func newMockResponseWriter() *mockResponseWriter { return &mockResponseWriter{ - headers: make(http.Header), - status: http.StatusOK, - writeError: true, + ResponseRecorder: httptest.NewRecorder(), + writeError: true, } } var brokenUrl = string(rune(0)) var errHTTP error = fmt.Errorf("bad http request") - -// Create a error reader to break json.Unmarshal() -type errReader int - -var errBodyRead error = fmt.Errorf("bad body read") - -func (errReader) Read(p []byte) (n int, err error) { - return 0, errBodyRead -} -func (errReader) Close() error { - return nil -} - -// Variables used in testing - -// Help function to create a test system -func createTestSystem(broken bool) (sys components.System) { - // instantiate the System - ctx := context.Background() - sys = components.NewSystem("testSystem", ctx) - - // Instantiate the Capsule - sys.Husk = &components.Husk{ - Description: "A test system", - Details: map[string][]string{"Developer": {"Test dev"}}, - ProtoPort: map[string]int{"https": 0, "http": 1234, "coap": 0}, - InfoLink: "https://for.testing.purposes", - } - - // create fake services and cervices for a mocked unit asset - testCerv := &components.Cervice{ - Definition: "testCerv", - Details: map[string][]string{"Forms": {"SignalA_v1a"}}, - Nodes: map[string][]string{}, - } - - CervicesMap := &components.Cervices{ - testCerv.Definition: testCerv, - } - setTest := &components.Service{ - ID: 1, - Definition: "squest", - SubPath: "squest", - Details: map[string][]string{"Forms": {"SignalA_v1a"}}, - Description: "A test service", - RegPeriod: 45, - RegTimestamp: "now", - RegExpiration: "45", - } - ServicesMap := &components.Services{ - setTest.SubPath: setTest, - } - - mua := &UnitAsset{ - Name: "testUnitAsset", - Details: map[string][]string{"Test": {"Test"}}, - leadingRegistrar: "", - ServicesMap: *ServicesMap, - CervicesMap: *CervicesMap, - } - - sys.UAssets = make(map[string]*components.UnitAsset) - var muaInterface components.UnitAsset = mua - sys.UAssets[mua.GetName()] = &muaInterface - - leadingRegistrar := &components.CoreSystem{ - Name: components.ServiceRegistrarName, - Url: "https://leadingregistrar", - } - test := &components.CoreSystem{ - Name: "test", - Url: "https://test", - } - if broken == false { - orchestrator := &components.CoreSystem{ - Name: "orchestrator", - Url: "https://orchestator", - } - sys.CoreS = []*components.CoreSystem{ - leadingRegistrar, - orchestrator, - test, - } - } else { - orchestrator := &components.CoreSystem{ - Name: "orchestrator", - Url: brokenUrl, - } - sys.CoreS = []*components.CoreSystem{ - leadingRegistrar, - orchestrator, - test, - } - } - return -} diff --git a/orchestrator/orchestrator_test.go b/orchestrator/orchestrator_test.go index 9088ffd..3c68716 100644 --- a/orchestrator/orchestrator_test.go +++ b/orchestrator/orchestrator_test.go @@ -3,7 +3,6 @@ package main import ( "encoding/json" "io" - "log" "net/http" "net/http/httptest" "strings" @@ -58,29 +57,23 @@ func TestServing(t *testing.T) { func createMultiHTTPResponse(limit int, writeError bool, body string) func() *http.Response { count := 0 return func() *http.Response { + resp := &http.Response{ + Status: "200 OK", + StatusCode: 200, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: nil, + } count++ if count == limit && writeError == true { - return &http.Response{ - Status: "200 OK", - StatusCode: 200, - Header: http.Header{"Content-Type": []string{"application/json"}}, - Body: io.NopCloser(errorReader{}), - } + resp.Body = io.NopCloser(errorReader{}) + return resp } if count == limit { - return &http.Response{ - Status: "200 OK", - StatusCode: 200, - Header: http.Header{"Content-Type": []string{"application/json"}}, - Body: io.NopCloser(strings.NewReader(body)), - } - } - return &http.Response{ - Status: "200 OK", - StatusCode: 200, - Header: http.Header{"Content-Type": []string{"application/json"}}, - Body: io.NopCloser(strings.NewReader(string("lead Service Registrar since"))), + resp.Body = io.NopCloser(strings.NewReader(body)) + return resp } + resp.Body = io.NopCloser(strings.NewReader(string("lead Service Registrar since"))) + return resp } } @@ -90,7 +83,7 @@ func createTestServiceQuestForm() []byte { serviceQuestForm.NewForm() fakebody, err := json.Marshal(serviceQuestForm) if err != nil { - log.Fatalf("Fail marshal at start of test: %v", err) + t.Fatalf("Fail marshal at start of test: %v", err) } return fakebody } @@ -102,7 +95,7 @@ func createTestServicePointForm() []byte { servicePointForm.ServLocation = "http://123.456.789:123//" fakebody, err := json.MarshalIndent(servicePointForm, "", " ") if err != nil { - log.Fatalf("Fail marshal at start of test: %v", err) + t.Fatalf("Fail marshal at start of test: %v", err) } return fakebody } @@ -124,7 +117,7 @@ func createTestServiceRecordListForm() []byte { serviceRecordListForm.List = []forms.ServiceRecord_v1{serviceRecordForm, serviceRecord2Form} fakebody, err := json.MarshalIndent(serviceRecordListForm, "", " ") if err != nil { - log.Fatalf("Fail marshal at start of test: %v", err) + t.Fatalf("Fail marshal at start of test: %v", err) } return fakebody } @@ -134,7 +127,6 @@ var getServiceURLErrorMessage = "core system 'serviceregistrar' not found: verif "(*main.mockTransport) returned a nil *Response with a nil error\n" type orchestrateTestStruct struct { - inputW http.ResponseWriter inputBody io.ReadCloser httpMethod string contentType string @@ -145,19 +137,17 @@ type orchestrateTestStruct struct { } var orchestrateTestParams = []orchestrateTestStruct{ - {httptest.NewRecorder(), io.NopCloser(strings.NewReader(string(createTestServiceQuestForm()))), "POST", + {io.NopCloser(strings.NewReader(string(createTestServiceQuestForm()))), "POST", "application/json", 3, 200, string(createTestServicePointForm()), "Best case, everything passes"}, - {httptest.NewRecorder(), io.NopCloser(strings.NewReader(string(createTestServiceQuestForm()))), "POST", + {io.NopCloser(strings.NewReader(string(createTestServiceQuestForm()))), "POST", "", 3, 200, "", "Bad case, header content type is wrong"}, - {httptest.NewRecorder(), io.NopCloser(errorReader{}), "POST", + {io.NopCloser(errorReader{}), "POST", "application/json", 3, 200, "", "Bad case, ReadAll on header body fails"}, - {httptest.NewRecorder(), io.NopCloser(strings.NewReader(string("hej hej"))), "POST", + {io.NopCloser(strings.NewReader(string("hej hej"))), "POST", "text/plain", 3, 200, "", "Bad case, Unpack and type assertion to ServiceQuest_v1 fails"}, - {httptest.NewRecorder(), io.NopCloser(strings.NewReader(string(createTestServiceQuestForm()))), "POST", + {io.NopCloser(strings.NewReader(string(createTestServiceQuestForm()))), "POST", "application/json", 1, 503, getServiceURLErrorMessage, "Bad case, getServiceURL fails"}, - {newMockResponseWriter(), io.NopCloser(strings.NewReader(string(createTestServiceQuestForm()))), "POST", - "application/json", 3, 500, "", "Bad case, write fails"}, - {httptest.NewRecorder(), io.NopCloser(strings.NewReader(string(""))), "PUT", + {io.NopCloser(strings.NewReader(string(""))), "PUT", "", 0, 404, "Method is not supported.\n", "Bad case, wrong http method"}, } @@ -168,29 +158,33 @@ func TestOrchestrate(t *testing.T) { mua := createUnitAsset() newMockTransport(createMultiHTTPResponse(2, false, string(createTestServiceRecordListForm())), testCase.mockTransportErr, nil) - testCase.inputW.Header() - mua.orchestrate(testCase.inputW, inputR) - - recorder, ok := testCase.inputW.(*httptest.ResponseRecorder) - if ok { - if recorder.Body.String() != testCase.expectedOutput || recorder.Code != testCase.expectedCode { - t.Errorf("In test case: %s: Expected %s, got: %s", - testCase.testName, testCase.expectedOutput, recorder.Body.String()) - } - } else { - if recorder, ok := testCase.inputW.(*mockResponseWriter); ok { - if recorder.status != testCase.expectedCode { - t.Errorf("Expected status %d, got %d", testCase.expectedCode, recorder.status) - } - } else { - t.Errorf("Expected inputW to be of type mockResponseWriter") - } + + inputW := httptest.NewRecorder() + inputW.Header() + mua.orchestrate(inputW, inputR) + + if inputW.Body.String() != testCase.expectedOutput || inputW.Result().StatusCode != testCase.expectedCode { + t.Errorf("In test case: %s: Expected %s, got: %s", + testCase.testName, testCase.expectedOutput, inputW.Body.String()) } } + + // Special case + inputR := httptest.NewRequest(http.MethodPost, "/test123", + io.NopCloser(strings.NewReader(string(createTestServiceQuestForm())))) + inputR.Header.Set("content-Type", "application/json") + mua := createUnitAsset() + newMockTransport(createMultiHTTPResponse(2, false, string(createTestServiceRecordListForm())), 3, nil) + inputW := newMockResponseWriter() + mua.orchestrate(inputW, inputR) + + if inputW.ResponseRecorder.Body.String() != "" || inputW.ResponseRecorder.Code != 500 { + t.Errorf("In test case: Bad case, write fails: Expected: , and: 500, got: %s, and: %d", + inputW.ResponseRecorder.Body.String(), inputW.ResponseRecorder.Code) + } } type orchestrateMultipleTestStruct struct { - inputW http.ResponseWriter inputBody io.ReadCloser httpMethod string contentType string @@ -201,19 +195,17 @@ type orchestrateMultipleTestStruct struct { } var orchestrateMultipleTestParams = []orchestrateMultipleTestStruct{ - {httptest.NewRecorder(), io.NopCloser(strings.NewReader(string(createTestServiceQuestForm()))), "POST", + {io.NopCloser(strings.NewReader(string(createTestServiceQuestForm()))), "POST", "application/json", 3, 200, string(createTestServiceRecordListForm()), "Best case, everything passes"}, - {httptest.NewRecorder(), io.NopCloser(strings.NewReader(string(createTestServiceQuestForm()))), "POST", + {io.NopCloser(strings.NewReader(string(createTestServiceQuestForm()))), "POST", "", 3, 200, "", "Bad case, header content type is wrong"}, - {httptest.NewRecorder(), io.NopCloser(errorReader{}), "POST", + {io.NopCloser(errorReader{}), "POST", "application/json", 3, 200, "", "Bad case, ReadAll on header body fails"}, - {httptest.NewRecorder(), io.NopCloser(strings.NewReader(string("hej hej"))), "POST", + {io.NopCloser(strings.NewReader(string("hej hej"))), "POST", "text/plain", 3, 200, "", "Bad case, Unpack and type assertion to ServiceQuest_v1 fails"}, - {httptest.NewRecorder(), io.NopCloser(strings.NewReader(string(createTestServiceQuestForm()))), "POST", + {io.NopCloser(strings.NewReader(string(createTestServiceQuestForm()))), "POST", "application/json", 1, 503, getServiceURLErrorMessage, "Bad case, getServiceURL fails"}, - {newMockResponseWriter(), io.NopCloser(strings.NewReader(string(createTestServiceQuestForm()))), "POST", - "application/json", 3, 0, "", "Bad case, write fails"}, - {httptest.NewRecorder(), io.NopCloser(strings.NewReader(string(""))), "PUT", + {io.NopCloser(strings.NewReader(string(""))), "PUT", "", 0, 404, "Method is not supported.\n", "Bad case, wrong http method"}, } @@ -224,18 +216,27 @@ func TestOrchestrateMultiple(t *testing.T) { mua := createUnitAsset() newMockTransport(createMultiHTTPResponse(2, false, string(createTestServiceRecordListForm())), testCase.mockTransportErr, nil) - mua.orchestrateMultiple(testCase.inputW, inputR) - - recorder, ok := testCase.inputW.(*httptest.ResponseRecorder) - if ok { - if recorder.Body.String() != testCase.expectedOutput || recorder.Code != testCase.expectedCode { - t.Errorf("In test case: %s: Expected %s, got: %s", - testCase.testName, testCase.expectedOutput, recorder.Body.String()) - } - } else { - if _, ok := testCase.inputW.(*mockResponseWriter); !ok { - t.Errorf("Expected inputW to be of type mockResponseWriter") - } + inputW := httptest.NewRecorder() + mua.orchestrateMultiple(inputW, inputR) + + if inputW.Body.String() != testCase.expectedOutput || inputW.Code != testCase.expectedCode { + t.Errorf("In test case: %s: Expected %s, got: %s", + testCase.testName, testCase.expectedOutput, inputW.Body.String()) } } + + // Special case, write fails + + inputR := httptest.NewRequest(http.MethodPost, "/test123", + io.NopCloser(strings.NewReader(string(createTestServiceQuestForm())))) + inputR.Header.Set("content-Type", "application/json") + mua := createUnitAsset() + newMockTransport(createMultiHTTPResponse(2, false, string(createTestServiceRecordListForm())), 3, nil) + inputW := newMockResponseWriter() + mua.orchestrateMultiple(inputW, inputR) + + if inputW.ResponseRecorder.Body.String() != "" || inputW.ResponseRecorder.Code != 500 { + t.Errorf("In test case: Bad case, write fails: Expected: , and: 500, got: %s, and: %d", + inputW.ResponseRecorder.Body.String(), inputW.ResponseRecorder.Code) + } } diff --git a/orchestrator/thing_test.go b/orchestrator/thing_test.go index 946e3cb..7e0f9ef 100644 --- a/orchestrator/thing_test.go +++ b/orchestrator/thing_test.go @@ -4,86 +4,15 @@ import ( "bytes" "encoding/json" "io" - "log" "net/http" "strings" "testing" - "github.com/sdoque/mbaigo/components" "github.com/sdoque/mbaigo/forms" "github.com/sdoque/mbaigo/usecases" ) -func TestInitTemplate(t *testing.T) { - expectedServices := []string{"squest", "squests"} - - ua := initTemplate() - services := ua.GetServices() - - // Check if expected name and services are present - if ua.GetName() != "orchestration" { - t.Errorf("Name mismatch expected 'registry', got: %s", ua.GetName()) - } - - for _, s := range expectedServices { - if _, ok := services[s]; !ok { - t.Errorf("Expected service '%s' to be present", s) - } - } -} - -func createConfAssetBrokenTraits() usecases.ConfigurableAsset { - brokenTrait, _ := json.Marshal(errReader(0)) - uac := usecases.ConfigurableAsset{ - Name: "testOrchestrator", - Details: map[string][]string{"testDetail": {"detail1", "detail2"}}, - Services: []components.Service{}, - Traits: []json.RawMessage{json.RawMessage(brokenTrait)}, - } - return uac -} - -func createConfAssetMultipleTraits() usecases.ConfigurableAsset { - uac := usecases.ConfigurableAsset{ - Name: "testOrchestrator", - Details: map[string][]string{"testDetail": {"detail1", "detail2"}}, - Services: []components.Service{}, - Traits: []json.RawMessage{json.RawMessage(`{"recCount": 0}`), json.RawMessage(`{"leading": false}`)}, - } - return uac -} - -type newResourceParams struct { - setup func() components.System - confAsset func() usecases.ConfigurableAsset - testCase string -} - -func TestNewResource(t *testing.T) { - params := []newResourceParams{ - { - func() (sys components.System) { return createTestSystem(false) }, - func() (confAsset usecases.ConfigurableAsset) { return createConfAssetBrokenTraits() }, - "Case: unmarshal traits fails", - }, - { - func() (sys components.System) { return createTestSystem(false) }, - func() (confAsset usecases.ConfigurableAsset) { return createConfAssetMultipleTraits() }, - "Case: confAsset has multiple traits", - }, - } - - for _, c := range params { - sys := c.setup() - uac := c.confAsset() - - ua, shutdown := newResource(uac, &sys) - shutdown() - if ua.GetName() != "testOrchestrator" { - t.Errorf("Name mismatch, expected '%s' got '%s'", uac.Name, ua.GetName()) - } - } -} +var t *testing.T func createTestServiceQuest() forms.ServiceQuest_v1 { var ServiceQuest_v1_temperature forms.ServiceQuest_v1 @@ -96,23 +25,21 @@ func createTestServiceQuest() forms.ServiceQuest_v1 { func (ua *UnitAsset) createDelayedBrokenURL(limit int) func() *http.Response { count := 0 return func() *http.Response { + resp := &http.Response{ + Status: "200 OK", + StatusCode: 200, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: nil, + } count++ if count == limit { f := createTestServiceRecordListForm() ua.leadingRegistrar = brokenUrl - return &http.Response{ - Status: "200 OK", - StatusCode: 200, - Header: http.Header{"Content-Type": []string{"application/json"}}, - Body: io.NopCloser(bytes.NewReader(f)), - } - } - return &http.Response{ - Status: "200 OK", - StatusCode: 200, - Header: http.Header{"Content-Type": []string{"application/json"}}, - Body: io.NopCloser(strings.NewReader(string("lead Service Registrar since"))), + resp.Body = io.NopCloser(bytes.NewReader(f)) + return resp } + resp.Body = io.NopCloser(strings.NewReader(string("lead Service Registrar since"))) + return resp } } @@ -122,7 +49,7 @@ func createEmptyServiceRecordListForm() []byte { emptyServiceRecordListForm.NewForm() fakebody, err := json.Marshal(emptyServiceRecordListForm) if err != nil { - log.Fatalf("Fail marshal at start of test: %v", err) + t.Fatalf("Fail marshal at start of test: %v", err) } return fakebody } @@ -179,11 +106,11 @@ func TestSelectService(t *testing.T) { serviceListbytes := createTestServiceRecordListForm() serviceListf, err := usecases.Unpack(serviceListbytes, "application/json") if err != nil { - log.Fatalf("Error setting up test of SelectService function: %v", err) + t.Fatalf("Error setting up test of SelectService function: %v", err) } serviceList, ok := serviceListf.(*forms.ServiceRecordList_v1) if !ok { - log.Fatalf("Error in type assertion when setting up test of SelectService function") + t.Fatalf("Error in type assertion when setting up test of SelectService function") } expectedService := createTestServicePointForm() @@ -217,7 +144,7 @@ func createTestServiceRecordListFormWithSeveral() []byte { serviceRecordFormRotation} fakebody, err := json.MarshalIndent(ServiceRecordListFormWithSeveral, "", " ") if err != nil { - log.Fatalf("Fail marshal at start of test: %v", err) + t.Fatalf("Fail marshal at start of test: %v", err) } return fakebody } @@ -233,7 +160,7 @@ func createTestServiceRecordListFormWithDefinition() []byte { serviceRecordListFormWithDefinition.List = []forms.ServiceRecord_v1{serviceRecordFormWithDefinition} fakebody, err := json.MarshalIndent(serviceRecordListFormWithDefinition, "", " ") if err != nil { - log.Fatalf("Fail marshal at start of test: %v", err) + t.Fatalf("Fail marshal at start of test: %v", err) } return fakebody } @@ -249,7 +176,7 @@ func createTestServiceRecordListFormWithDetails() []byte { serviceRecordListFormWithDetails.List = []forms.ServiceRecord_v1{serviceRecordFormWithDetails} fakebody, err := json.MarshalIndent(serviceRecordListFormWithDetails, "", " ") if err != nil { - log.Fatalf("Fail marshal at start of test: %v", err) + t.Fatalf("Fail marshal at start of test: %v", err) } return fakebody } From 61f0df264bde0b4c02989b38e138f1d12803fd1b Mon Sep 17 00:00:00 2001 From: gabaxh Date: Mon, 21 Jul 2025 14:48:34 +0200 Subject: [PATCH 33/81] Removed the use of testing variable in functions creating forms --- orchestrator/orchestrator_test.go | 7 ++++--- orchestrator/thing_test.go | 11 +++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/orchestrator/orchestrator_test.go b/orchestrator/orchestrator_test.go index 3c68716..bc3fc54 100644 --- a/orchestrator/orchestrator_test.go +++ b/orchestrator/orchestrator_test.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "fmt" "io" "net/http" "net/http/httptest" @@ -83,7 +84,7 @@ func createTestServiceQuestForm() []byte { serviceQuestForm.NewForm() fakebody, err := json.Marshal(serviceQuestForm) if err != nil { - t.Fatalf("Fail marshal at start of test: %v", err) + panic(fmt.Sprintf("Fail marshal at start of test: %v", err)) } return fakebody } @@ -95,7 +96,7 @@ func createTestServicePointForm() []byte { servicePointForm.ServLocation = "http://123.456.789:123//" fakebody, err := json.MarshalIndent(servicePointForm, "", " ") if err != nil { - t.Fatalf("Fail marshal at start of test: %v", err) + panic(fmt.Sprintf("Fail marshal at start of test: %v", err)) } return fakebody } @@ -117,7 +118,7 @@ func createTestServiceRecordListForm() []byte { serviceRecordListForm.List = []forms.ServiceRecord_v1{serviceRecordForm, serviceRecord2Form} fakebody, err := json.MarshalIndent(serviceRecordListForm, "", " ") if err != nil { - t.Fatalf("Fail marshal at start of test: %v", err) + panic(fmt.Sprintf("Fail marshal at start of test: %v", err)) } return fakebody } diff --git a/orchestrator/thing_test.go b/orchestrator/thing_test.go index 7e0f9ef..309271c 100644 --- a/orchestrator/thing_test.go +++ b/orchestrator/thing_test.go @@ -3,6 +3,7 @@ package main import ( "bytes" "encoding/json" + "fmt" "io" "net/http" "strings" @@ -12,8 +13,6 @@ import ( "github.com/sdoque/mbaigo/usecases" ) -var t *testing.T - func createTestServiceQuest() forms.ServiceQuest_v1 { var ServiceQuest_v1_temperature forms.ServiceQuest_v1 ServiceQuest_v1_temperature.NewForm() @@ -49,7 +48,7 @@ func createEmptyServiceRecordListForm() []byte { emptyServiceRecordListForm.NewForm() fakebody, err := json.Marshal(emptyServiceRecordListForm) if err != nil { - t.Fatalf("Fail marshal at start of test: %v", err) + panic(fmt.Sprintf("Fail marshal at start of test: %v", err)) } return fakebody } @@ -144,7 +143,7 @@ func createTestServiceRecordListFormWithSeveral() []byte { serviceRecordFormRotation} fakebody, err := json.MarshalIndent(ServiceRecordListFormWithSeveral, "", " ") if err != nil { - t.Fatalf("Fail marshal at start of test: %v", err) + panic(fmt.Sprintf("Fail marshal at start of test: %v", err)) } return fakebody } @@ -160,7 +159,7 @@ func createTestServiceRecordListFormWithDefinition() []byte { serviceRecordListFormWithDefinition.List = []forms.ServiceRecord_v1{serviceRecordFormWithDefinition} fakebody, err := json.MarshalIndent(serviceRecordListFormWithDefinition, "", " ") if err != nil { - t.Fatalf("Fail marshal at start of test: %v", err) + panic(fmt.Sprintf("Fail marshal at start of test: %v", err)) } return fakebody } @@ -176,7 +175,7 @@ func createTestServiceRecordListFormWithDetails() []byte { serviceRecordListFormWithDetails.List = []forms.ServiceRecord_v1{serviceRecordFormWithDetails} fakebody, err := json.MarshalIndent(serviceRecordListFormWithDetails, "", " ") if err != nil { - t.Fatalf("Fail marshal at start of test: %v", err) + panic(fmt.Sprintf("Fail marshal at start of test: %v", err)) } return fakebody } From b77f405893cdf5c0cd4631c23a23f6fd65469e13 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 22 Jul 2025 13:50:34 +0200 Subject: [PATCH 34/81] Clean ups --- messenger/dashboard.html | 2 +- messenger/messenger.go | 6 ++-- messenger/thing.go | 77 +++++++++++++--------------------------- 3 files changed, 29 insertions(+), 56 deletions(-) diff --git a/messenger/dashboard.html b/messenger/dashboard.html index 1e438eb..d504da8 100644 --- a/messenger/dashboard.html +++ b/messenger/dashboard.html @@ -51,7 +51,7 @@

Log

    -{{range .Logs}} +{{range .Latest}}
  • {{.}}
  • {{else}}
  • No logs.
  • diff --git a/messenger/messenger.go b/messenger/messenger.go index 4f7a47e..b31e02e 100644 --- a/messenger/messenger.go +++ b/messenger/messenger.go @@ -62,7 +62,7 @@ func main() { usecases.RegisterServices(&sys) go usecases.SetoutServers(&sys) <-sys.Sigs - usecases.LogInfo(&sys, "shuting down %s", sys.Name) + usecases.LogInfo(&sys, "shutting down %s", sys.Name) cancel() time.Sleep(2 * time.Second) } @@ -108,11 +108,11 @@ func (ua *UnitAsset) handleNewMessage(w http.ResponseWriter, r *http.Request) { } func (ua *UnitAsset) handleDashboard(w http.ResponseWriter, r *http.Request) { - errors, warnings := ua.latestWarnings() + errors, warnings, latest := ua.filterLogs() data := map[string]any{ "Errors": errors, "Warnings": warnings, - "Logs": ua.latestLogs(), + "Latest": latest, } buf := &bytes.Buffer{} diff --git a/messenger/thing.go b/messenger/thing.go index 0ebd9bb..53a7f43 100644 --- a/messenger/thing.go +++ b/messenger/thing.go @@ -19,9 +19,6 @@ import ( "github.com/sdoque/mbaigo/usecases" ) -//go:embed dashboard.html -var tmplDashboard string - type message struct { time time.Time level forms.MessageLevel @@ -38,24 +35,19 @@ func (m message) String() string { ) } -// type Traits struct { -// } - type UnitAsset struct { Name string `json:"name"` Owner *components.System `json:"-"` Details map[string][]string `json:"details"` ServicesMap components.Services `json:"-"` CervicesMap components.Cervices `json:"-"` - // Traits - regMsg []byte // Cached MessengerRegistration form - messages map[string][]message - mutex sync.RWMutex - tmplDashboard *template.Template + regMsg []byte // Cached MessengerRegistration form + messages map[string][]message // Per system msg log + mutex sync.RWMutex // Protects concurrent access to previous field + tmplDashboard *template.Template // The HTML template loaded from file } -// TODO: check if pointer is necessary?? func (ua *UnitAsset) GetName() string { return ua.Name } func (ua *UnitAsset) GetServices() components.Services { return ua.ServicesMap } @@ -64,8 +56,6 @@ func (ua *UnitAsset) GetCervices() components.Cervices { return ua.CervicesMap } func (ua *UnitAsset) GetDetails() map[string][]string { return ua.Details } -// func (ua *UnitAsset) GetTraits() any { return ua.Traits } - var _ components.UnitAsset = (*UnitAsset)(nil) func initTemplate() components.UnitAsset { @@ -83,6 +73,11 @@ func initTemplate() components.UnitAsset { } } +// Instructs the compiler to load and embed the following file into the built binary + +//go:embed dashboard.html +var tmplDashboard string + func newResource(ca usecases.ConfigurableAsset, sys *components.System) (components.UnitAsset, func(), error) { ua := &UnitAsset{ Name: ca.Name, @@ -91,18 +86,12 @@ func newResource(ca usecases.ConfigurableAsset, sys *components.System) (compone ServicesMap: usecases.MakeServiceMap(ca.Services), messages: make(map[string][]message), } - // traits, err := unmarshalTraits(ca.Traits) - // if err != nil { - // return nil, nil, err - // } - // ua.Traits = traits[0] var err error ua.tmplDashboard, err = template.New("dashboard").Parse(tmplDashboard) if err != nil { return nil, nil, err } - ua.regMsg, err = newRegMsg(sys) if err != nil { return nil, nil, err @@ -112,18 +101,6 @@ func newResource(ca usecases.ConfigurableAsset, sys *components.System) (compone return ua, f, nil } -// func unmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { -// var traitsList []Traits -// for _, raw := range rawTraits { -// var t Traits -// if err := json.Unmarshal(raw, &t); err != nil { -// return nil, fmt.Errorf("unmarshal trait: %w", err) -// } -// traitsList = append(traitsList, t) -// } -// return traitsList, nil -// } - //////////////////////////////////////////////////////////////////////////////// // newRegMsg creates a new MessengerRegistration form filled with the system's URL. @@ -149,7 +126,7 @@ func newRegMsg(sys *components.System) ([]byte, error) { return usecases.Pack(forms.Form(&m), "application/json") } -const timeoutUpdate int = 30 +const beaconPeriod int = 30 // runBeacon runs periodically in the background (in a goroutine at startup). // It fetches a list of systems and then sends out a MessengerRegistration to each. @@ -161,7 +138,7 @@ func (ua *UnitAsset) runBeacon() { } ua.notifySystems(s) select { - case <-time.Tick(time.Duration(timeoutUpdate) * time.Second): + case <-time.Tick(time.Duration(beaconPeriod) * time.Second): case <-ua.Owner.Ctx.Done(): return } @@ -219,16 +196,19 @@ func (ua *UnitAsset) notifySystems(list []string) { continue // Skip itself and other messengers } // Don't care about any errors or any systems that don't want to talk with us + // (using empty variable names to shut up the linter warning about unhandled errors) _, _ = sendRequest("POST", sys+"/msg", ua.regMsg) } } const maxMessages int = 10 +// addMessage adds the new message m to a system's log and optionally removes the +// oldest, if the log's size is larger than maxMessages. +// Note that this function sets the timestamp of the incoming msg too. func (ua *UnitAsset) addMessage(m forms.SystemMessage_v1) { ua.mutex.Lock() defer ua.mutex.Unlock() - ua.messages[m.System] = append(ua.messages[m.System], message{ time: time.Now(), level: m.Level, @@ -240,14 +220,17 @@ func (ua *UnitAsset) addMessage(m forms.SystemMessage_v1) { } } -func (ua *UnitAsset) latestWarnings() (errors, warnings map[string]message) { +// filterLogs fetches the latest errors/warnings/all messages from the log. +// The log is appended to in a chronological order already, so the latest error +// and warning for each system will be returned and "all" will be in reverse +// chronological order. +func (ua *UnitAsset) filterLogs() (errors, warnings map[string]message, all []message) { errors = make(map[string]message) warnings = make(map[string]message) ua.mutex.RLock() - defer ua.mutex.RUnlock() - for system := range ua.messages { for _, m := range ua.messages[system] { + all = append(all, m) switch m.level { case forms.LevelError: errors[system] = m @@ -256,20 +239,10 @@ func (ua *UnitAsset) latestWarnings() (errors, warnings map[string]message) { } } } - return -} - -func (ua *UnitAsset) latestLogs() (logs []message) { - ua.mutex.RLock() - defer ua.mutex.RUnlock() - - for system := range ua.messages { - for _, m := range ua.messages[system] { - logs = append(logs, m) - } - } - sort.Slice(logs, func(i, j int) bool { - return logs[i].time.After(logs[j].time) + ua.mutex.RUnlock() + // Reverse order + sort.Slice(all, func(i, j int) bool { + return all[i].time.After(all[j].time) }) return } From 3ad416d6a387bed800c5b504af0f7aa46daf2b4b Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 25 Jul 2025 14:10:51 +0200 Subject: [PATCH 35/81] Adds tests for messenger.go It was surprisingly hard to find a way to test template.Execute() errors. --- messenger/messenger.go | 35 ++++++++++-- messenger/messenger_test.go | 103 ++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 messenger/messenger_test.go diff --git a/messenger/messenger.go b/messenger/messenger.go index b31e02e..66393a3 100644 --- a/messenger/messenger.go +++ b/messenger/messenger.go @@ -5,8 +5,10 @@ import ( "context" "crypto/x509/pkix" "encoding/json" + "fmt" "io" "net/http" + "testing" "time" "github.com/sdoque/mbaigo/components" @@ -85,7 +87,6 @@ func (ua *UnitAsset) handleNewMessage(w http.ResponseWriter, r *http.Request) { } b, err := io.ReadAll(r.Body) if err != nil { - usecases.LogError(ua.Owner, "read request body: %v", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return @@ -94,20 +95,41 @@ func (ua *UnitAsset) handleNewMessage(w http.ResponseWriter, r *http.Request) { f, err := usecases.Unpack(b, r.Header.Get("Content-Type")) if err != nil { - usecases.LogWarn(ua.Owner, "unpack: %v", err) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } msg, ok := f.(*forms.SystemMessage_v1) if !ok { - usecases.LogWarn(ua.Owner, "form is not a SystemMessage_v1") http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } ua.addMessage(*msg) // Don't want to have to deal with pointers, hence the * } +// Encapsulates the regular bytes.Buffer, in order to allow causing mock errors +type mockableBuffer struct { + bytes.Buffer // This embedded struct is available as "mockableBuffer.Buffer" by default + errWrite error +} + +func (mock *mockableBuffer) setWriteError(err error) { + mock.errWrite = err +} + +func (mock *mockableBuffer) Write(body []byte) (int, error) { + if mock.errWrite != nil { + return 0, mock.errWrite + } + return mock.Buffer.Write(body) +} + +const testBufferHeader string = "x-testing-buffer" + func (ua *UnitAsset) handleDashboard(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } errors, warnings, latest := ua.filterLogs() data := map[string]any{ "Errors": errors, @@ -115,7 +137,12 @@ func (ua *UnitAsset) handleDashboard(w http.ResponseWriter, r *http.Request) { "Latest": latest, } - buf := &bytes.Buffer{} + buf := &mockableBuffer{} + // Protects the special test header by enabling it's use only while running `go test` + if testing.Testing() && r.Header.Get(testBufferHeader) != "" { + // This write error will cause an error in the template.Execute() below + buf.setWriteError(fmt.Errorf("mock error")) + } if err := ua.tmplDashboard.Execute(buf, data); err != nil { usecases.LogError(ua.Owner, "execute dashboard: %s", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) diff --git a/messenger/messenger_test.go b/messenger/messenger_test.go new file mode 100644 index 0000000..0f3a14d --- /dev/null +++ b/messenger/messenger_test.go @@ -0,0 +1,103 @@ +package main + +import ( + "context" + "fmt" + "html/template" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/sdoque/mbaigo/components" +) + +type errorReader struct{} + +func (er *errorReader) Read(b []byte) (int, error) { + return 0, fmt.Errorf("read error") +} + +func (er *errorReader) Close() error { + return fmt.Errorf("close error") +} + +func TestHandleNewMessage(t *testing.T) { + table := []struct { + expectedStatus int + method string + content string + body io.ReadCloser + }{ + // Method not post + {http.StatusMethodNotAllowed, http.MethodGet, "", nil}, + // Read body error + {http.StatusInternalServerError, http.MethodPost, "", &errorReader{}}, + // Unpack error + {http.StatusBadRequest, http.MethodPost, "bad type", nil}, + // Wrong form + {http.StatusBadRequest, http.MethodPost, "application/json", + io.NopCloser(strings.NewReader(`{"version":"MessengerRegistration_v1"}`)), + }, + // All ok + {http.StatusOK, http.MethodPost, "application/json", + io.NopCloser(strings.NewReader(`{"version":"SystemMessage_v1","system":"test"}`)), + }, + } + + ua := &UnitAsset{ + messages: make(map[string][]message), + } + for _, test := range table { + w := httptest.NewRecorder() + r := httptest.NewRequest(test.method, "/message", test.body) + r.Header.Set("Content-Type", test.content) + ua.handleNewMessage(w, r) + + res := w.Result() + if got, want := res.StatusCode, test.expectedStatus; got != want { + t.Errorf("expected status %d, got %d", want, got) + } + } +} + +func TestHandleDashboard(t *testing.T) { + table := []struct { + expectedStatus int + method string + badMockBuff bool + }{ + // Method not GET + {http.StatusMethodNotAllowed, http.MethodPost, false}, + // Template fails executing + {http.StatusInternalServerError, http.MethodGet, true}, + // All ok + {http.StatusOK, http.MethodGet, false}, + } + + tmpl, err := template.New("dashboard").Parse(tmplDashboard) + if err != nil { + t.Fatalf("expected no error from template.Parse, got %v", err) + } + sys := components.NewSystem("test sys", context.Background()) + ua := &UnitAsset{ + Owner: &sys, + messages: make(map[string][]message), + tmplDashboard: tmpl, + } + for _, test := range table { + rec := httptest.NewRecorder() + req := httptest.NewRequest(test.method, "/message", nil) + if test.badMockBuff { + // Triggers the mock buffer error + req.Header.Set(testBufferHeader, "true") + } + ua.handleDashboard(rec, req) + + res := rec.Result() + if got, want := res.StatusCode, test.expectedStatus; got != want { + t.Errorf("expected status %d, got %d", want, got) + } + } +} From 301d2679a22c68aaaa05d37249b00f7a9e9deb29 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 25 Jul 2025 14:20:39 +0200 Subject: [PATCH 36/81] Replaces single letter variables with better names --- messenger/messenger.go | 6 ++-- messenger/messenger_test.go | 12 +++---- messenger/thing.go | 70 ++++++++++++++++++------------------- 3 files changed, 44 insertions(+), 44 deletions(-) diff --git a/messenger/messenger.go b/messenger/messenger.go index 66393a3..5de9a29 100644 --- a/messenger/messenger.go +++ b/messenger/messenger.go @@ -85,7 +85,7 @@ func (ua *UnitAsset) handleNewMessage(w http.ResponseWriter, r *http.Request) { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } - b, err := io.ReadAll(r.Body) + body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -93,12 +93,12 @@ func (ua *UnitAsset) handleNewMessage(w http.ResponseWriter, r *http.Request) { } defer r.Body.Close() - f, err := usecases.Unpack(b, r.Header.Get("Content-Type")) + form, err := usecases.Unpack(body, r.Header.Get("Content-Type")) if err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } - msg, ok := f.(*forms.SystemMessage_v1) + msg, ok := form.(*forms.SystemMessage_v1) if !ok { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return diff --git a/messenger/messenger_test.go b/messenger/messenger_test.go index 0f3a14d..be9d0e3 100644 --- a/messenger/messenger_test.go +++ b/messenger/messenger_test.go @@ -15,7 +15,7 @@ import ( type errorReader struct{} -func (er *errorReader) Read(b []byte) (int, error) { +func (er *errorReader) Read([]byte) (int, error) { return 0, fmt.Errorf("read error") } @@ -50,12 +50,12 @@ func TestHandleNewMessage(t *testing.T) { messages: make(map[string][]message), } for _, test := range table { - w := httptest.NewRecorder() - r := httptest.NewRequest(test.method, "/message", test.body) - r.Header.Set("Content-Type", test.content) - ua.handleNewMessage(w, r) + rec := httptest.NewRecorder() + req := httptest.NewRequest(test.method, "/message", test.body) + req.Header.Set("Content-Type", test.content) + ua.handleNewMessage(rec, req) - res := w.Result() + res := rec.Result() if got, want := res.StatusCode, test.expectedStatus; got != want { t.Errorf("expected status %d, got %d", want, got) } diff --git a/messenger/thing.go b/messenger/thing.go index 53a7f43..ad7403f 100644 --- a/messenger/thing.go +++ b/messenger/thing.go @@ -42,7 +42,7 @@ type UnitAsset struct { ServicesMap components.Services `json:"-"` CervicesMap components.Cervices `json:"-"` - regMsg []byte // Cached MessengerRegistration form + cachedRegMsg []byte // Caches the MessengerRegistration form messages map[string][]message // Per system msg log mutex sync.RWMutex // Protects concurrent access to previous field tmplDashboard *template.Template // The HTML template loaded from file @@ -59,7 +59,7 @@ func (ua *UnitAsset) GetDetails() map[string][]string { return ua.Details } var _ components.UnitAsset = (*UnitAsset)(nil) func initTemplate() components.UnitAsset { - s := components.Service{ + service := components.Service{ Definition: "message", SubPath: "message", Details: map[string][]string{"Forms": {"SystemMessage_v1"}}, @@ -69,7 +69,7 @@ func initTemplate() components.UnitAsset { return &UnitAsset{ Name: "log", Details: map[string][]string{}, - ServicesMap: components.Services{s.SubPath: &s}, + ServicesMap: components.Services{service.SubPath: &service}, } } @@ -92,7 +92,7 @@ func newResource(ca usecases.ConfigurableAsset, sys *components.System) (compone if err != nil { return nil, nil, err } - ua.regMsg, err = newRegMsg(sys) + ua.cachedRegMsg, err = newRegMsg(sys) if err != nil { return nil, nil, err } @@ -109,21 +109,21 @@ func newRegMsg(sys *components.System) ([]byte, error) { // This system URL is created in the same way as the registrar, // in getUniqueSystems(). Using url.URL instead for safer string assembly. // https://github.com/lmas/mbaigo_systems/blob/dev/esr/thing.go#L404-L407 - var u url.URL - u.Host = sys.Host.IPAddresses[0] - u.Scheme = "https" - port := sys.Husk.ProtoPort[u.Scheme] + var systemURL url.URL + systemURL.Host = sys.Host.IPAddresses[0] + systemURL.Scheme = "https" + port := sys.Husk.ProtoPort[systemURL.Scheme] if port == 0 { - u.Scheme = "http" - port = sys.Husk.ProtoPort[u.Scheme] + systemURL.Scheme = "http" + port = sys.Husk.ProtoPort[systemURL.Scheme] if port == 0 { return nil, fmt.Errorf("no http(s) port defined in conf") } } - u.Host += ":" + strconv.Itoa(port) - u.Path = sys.Name - m := forms.NewMessengerRegistration_v1(u.String()) - return usecases.Pack(forms.Form(&m), "application/json") + systemURL.Host += ":" + strconv.Itoa(port) + systemURL.Path = sys.Name + registration := forms.NewMessengerRegistration_v1(systemURL.String()) + return usecases.Pack(forms.Form(®istration), "application/json") } const beaconPeriod int = 30 @@ -132,11 +132,11 @@ const beaconPeriod int = 30 // It fetches a list of systems and then sends out a MessengerRegistration to each. func (ua *UnitAsset) runBeacon() { for { - s, err := ua.fetchSystems() + systems, err := ua.fetchSystems() if err != nil { usecases.LogWarn(ua.Owner, "error fetching system list: %s", err) } - ua.notifySystems(s) + ua.notifySystems(systems) select { case <-time.Tick(time.Duration(beaconPeriod) * time.Second): case <-ua.Owner.Ctx.Done(): @@ -171,33 +171,33 @@ func (ua *UnitAsset) fetchSystems() (systems []string, err error) { if err != nil { return } - b, err := sendRequest("GET", url+"/syslist", nil) - f, err := usecases.Unpack(b, "application/json") + body, err := sendRequest("GET", url+"/syslist", nil) + form, err := usecases.Unpack(body, "application/json") if err != nil { return } - list, ok := f.(*forms.SystemRecordList_v1) + records, ok := form.(*forms.SystemRecordList_v1) if !ok { err = fmt.Errorf("form is not a SystemRecordList_v1") return } - return list.List, nil + return records.List, nil } // notifySystems sends a pre-packed MessengerRegistration form to a list of online systems. // Any systems with incorrect URLs, any messengers, and any http errors will be ignored. func (ua *UnitAsset) notifySystems(list []string) { for _, sys := range list { - u, err := url.Parse(sys) + sysURL, err := url.Parse(sys) if err != nil { continue // Skip misconfigured systems } - if strings.HasPrefix(u.Path, "/"+ua.Owner.Name) { + if strings.HasPrefix(sysURL.Path, "/"+ua.Owner.Name) { continue // Skip itself and other messengers } // Don't care about any errors or any systems that don't want to talk with us // (using empty variable names to shut up the linter warning about unhandled errors) - _, _ = sendRequest("POST", sys+"/msg", ua.regMsg) + _, _ = sendRequest("POST", sys+"/msg", ua.cachedRegMsg) } } @@ -206,17 +206,17 @@ const maxMessages int = 10 // addMessage adds the new message m to a system's log and optionally removes the // oldest, if the log's size is larger than maxMessages. // Note that this function sets the timestamp of the incoming msg too. -func (ua *UnitAsset) addMessage(m forms.SystemMessage_v1) { +func (ua *UnitAsset) addMessage(msg forms.SystemMessage_v1) { ua.mutex.Lock() defer ua.mutex.Unlock() - ua.messages[m.System] = append(ua.messages[m.System], message{ + ua.messages[msg.System] = append(ua.messages[msg.System], message{ time: time.Now(), - level: m.Level, - system: m.System, - body: m.Body, + level: msg.Level, + system: msg.System, + body: msg.Body, }) - if len(ua.messages[m.System]) > maxMessages { - ua.messages[m.System] = ua.messages[m.System][1:] + if len(ua.messages[msg.System]) > maxMessages { + ua.messages[msg.System] = ua.messages[msg.System][1:] } } @@ -229,13 +229,13 @@ func (ua *UnitAsset) filterLogs() (errors, warnings map[string]message, all []me warnings = make(map[string]message) ua.mutex.RLock() for system := range ua.messages { - for _, m := range ua.messages[system] { - all = append(all, m) - switch m.level { + for _, msg := range ua.messages[system] { + all = append(all, msg) + switch msg.level { case forms.LevelError: - errors[system] = m + errors[system] = msg case forms.LevelWarn: - warnings[system] = m + warnings[system] = msg } } } From 69421e8a6377eb34719ee9b3f4f5dfde0d6b5bee Mon Sep 17 00:00:00 2001 From: Pake Date: Mon, 14 Jul 2025 00:04:48 +0200 Subject: [PATCH 37/81] Fixed typos --- esr/esr.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/esr/esr.go b/esr/esr.go index 7b6d655..9b62f0b 100644 --- a/esr/esr.go +++ b/esr/esr.go @@ -43,7 +43,7 @@ func main() { // instantiate the System sys := components.NewSystem("serviceregistrar", ctx) - // Instatiate the Capusle + // Instantiate the Capsule sys.Husk = &components.Husk{ Description: "is an Arrowhead mandatory core system that keeps track of the currently available services.", Details: map[string][]string{"Developer": {"Synecdoque"}}, @@ -206,15 +206,15 @@ func (ua *UnitAsset) queryDB(w http.ResponseWriter, r *http.Request) { w.Write([]byte(text)) text = "

    The local cloud's currently available services are:

      " w.Write([]byte(text)) - for _, serRec := range servvicesList { + for _, servRec := range servvicesList { metaservice := "" - for key, values := range serRec.Details { + for key, values := range servRec.Details { metaservice += key + ": " + fmt.Sprintf("%v", values) + " " } - hyperlink := "http://" + serRec.IPAddresses[0] + ":" + strconv.Itoa(int(serRec.ProtoPort["http"])) + "/" + serRec.SystemName + "/" + serRec.SubPath - parts := strings.Split(serRec.SubPath, "/") + hyperlink := "http://" + servRec.IPAddresses[0] + ":" + strconv.Itoa(int(servRec.ProtoPort["http"])) + "/" + servRec.SystemName + "/" + servRec.SubPath + parts := strings.Split(servRec.SubPath, "/") uaName := parts[0] - sLine := "

      Service ID: " + strconv.Itoa(int(serRec.Id)) + " with definition " + serRec.ServiceDefinition + " from the " + serRec.SystemName + "/" + uaName + " with details " + metaservice + " will expire at: " + serRec.EndOfValidity + "

      " + sLine := "

      Service ID: " + strconv.Itoa(int(servRec.Id)) + " with definition " + servRec.ServiceDefinition + " from the " + servRec.SystemName + "/" + uaName + " with details " + metaservice + " will expire at: " + servRec.EndOfValidity + "

      " w.Write([]byte(fmt.Sprintf("
    • %s
    • ", sLine))) } text = "
    " From e5a929a0100c0b3b5597272c03f92e96ba6c61da Mon Sep 17 00:00:00 2001 From: Pake Date: Mon, 14 Jul 2025 00:05:28 +0200 Subject: [PATCH 38/81] Added test for initTemplate() --- esr/thing_test.go | 49 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 esr/thing_test.go diff --git a/esr/thing_test.go b/esr/thing_test.go new file mode 100644 index 0000000..b86d980 --- /dev/null +++ b/esr/thing_test.go @@ -0,0 +1,49 @@ +package main + +import "testing" + +// ----------------------------------------------------- // +// Help functions and structs to tests initTemplate() +// ----------------------------------------------------- // + +func TestInitTemplate(t *testing.T) { + expectedServices := []string{"register", "query", "unregister", "status"} + + ua := initTemplate() + services := ua.GetServices() + + // Check if expected name and services are present + if ua.GetName() != "registry" { + t.Errorf("Name mismatch expected 'registry', got: %s", ua.GetName()) + } + + for _, s := range expectedServices { + if _, ok := services[s]; !ok { + t.Errorf("Expected service '%s' to be present", s) + } + } +} + +// --------------------------------------------- // +// Help functions and structs to test *** +// --------------------------------------------- // + +// newResource(configuredAsset usecases.ConfigurableAsset, sys *components.System) (components.UnitAsset, func()) {...} +func TestNewResource(t *testing.T) {} + +// UnmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) {} +func TestUnmarshalTraits(t *testing.T) {} + +// (ua *UnitAsset) serviceRegistryHandler() {} +func TestServiceRegistryHandler(t *testing.T) {} + +// (ua *UnitAsset) FilterByServiceDefinitionAndDetails(desiredDefinition string, requiredDetails map[string][]string) []forms.ServiceRecord_v1 { +func TestFilterByServiceDefAndDetails(t *testing.T) {} + +// checkExpiration(ua *UnitAsset, servId int) {} +func TestCheckExpiration(t *testing.T) { + t.Errorf("Next test in line") +} + +// getUniqueSystems(ua *UnitAsset) (*forms.SystemRecordList_v1, error) {} +func TestGetUniqueSystems(t *testing.T) {} From d4e3d5fc76c751551b7f486b31ea0feffdd3e019 Mon Sep 17 00:00:00 2001 From: walpat-1 Date: Mon, 14 Jul 2025 15:35:04 +0200 Subject: [PATCH 39/81] Removed unnecessary logic --- esr/thing.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/esr/thing.go b/esr/thing.go index 332f506..c8e8888 100644 --- a/esr/thing.go +++ b/esr/thing.go @@ -396,14 +396,13 @@ func getUniqueSystems(ua *UnitAsset) (*forms.SystemRecordList_v1, error) { ua.mu.Lock() // Ensure thread safety defer ua.mu.Unlock() - for _, record := range ua.serviceRegistry { var sAddress string // Check for HTTPS - if port, exists := record.ProtoPort["https"]; exists && port != 0 { + if port := record.ProtoPort["https"]; port != 0 { sAddress = "https://" + record.IPAddresses[0] + ":" + strconv.Itoa(port) + "/" + record.SystemName - } else if port, exists := record.ProtoPort["http"]; exists && port != 0 { // Check for HTTP + } else if port := record.ProtoPort["http"]; port != 0 { // Check for HTTP sAddress = "http://" + record.IPAddresses[0] + ":" + strconv.Itoa(port) + "/" + record.SystemName } else { fmt.Printf("Warning: %s cannot be modeled\n", record.SystemName) From 66319c0efc8a679f845531b7edbb21fcf5619586 Mon Sep 17 00:00:00 2001 From: walpat-1 Date: Mon, 14 Jul 2025 15:35:41 +0200 Subject: [PATCH 40/81] Added test for getUniqueSystems() --- esr/thing_test.go | 102 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 97 insertions(+), 5 deletions(-) diff --git a/esr/thing_test.go b/esr/thing_test.go index b86d980..c0a4f69 100644 --- a/esr/thing_test.go +++ b/esr/thing_test.go @@ -1,6 +1,11 @@ package main -import "testing" +import ( + "fmt" + "testing" + + "github.com/sdoque/mbaigo/forms" +) // ----------------------------------------------------- // // Help functions and structs to tests initTemplate() @@ -41,9 +46,96 @@ func TestServiceRegistryHandler(t *testing.T) {} func TestFilterByServiceDefAndDetails(t *testing.T) {} // checkExpiration(ua *UnitAsset, servId int) {} -func TestCheckExpiration(t *testing.T) { - t.Errorf("Next test in line") +func TestCheckExpiration(t *testing.T) {} + +// ----------------------------------------------------- // +// Help functions and structs to test getUniqueSystems() +// ----------------------------------------------------- // + +func createServRegistryHttp() (ua *UnitAsset, err error) { + initTemp := initTemplate() + ua, ok := initTemp.(*UnitAsset) + if !ok { + return nil, fmt.Errorf("Failed while typecasting to local UnitAsset") + } + + var test forms.ServiceRecord_v1 + test.SystemName = "testSystem" + test.ProtoPort = map[string]int{"http": 1234} + test.IPAddresses = []string{"999.999.999.999"} + ua.serviceRegistry = map[int]forms.ServiceRecord_v1{0: test} + + return ua, nil +} + +func createServRegistryHttps() (ua *UnitAsset, err error) { + initTemp := initTemplate() + ua, ok := initTemp.(*UnitAsset) + if !ok { + return nil, fmt.Errorf("Failed while typecasting to local UnitAsset") + } + + var test forms.ServiceRecord_v1 + test.SystemName = "testSystem" + test.ProtoPort = map[string]int{"https": 4321} + test.IPAddresses = []string{"888.888.888.888"} + ua.serviceRegistry = map[int]forms.ServiceRecord_v1{0: test} + + return ua, nil } -// getUniqueSystems(ua *UnitAsset) (*forms.SystemRecordList_v1, error) {} -func TestGetUniqueSystems(t *testing.T) {} +func createBrokenServRegistry() (ua *UnitAsset, err error) { + initTemp := initTemplate() + ua, ok := initTemp.(*UnitAsset) + if !ok { + return nil, fmt.Errorf("Failed while typecasting to local UnitAsset") + } + + var test forms.ServiceRecord_v1 + test.SystemName = "testSystem" + test.ProtoPort = map[string]int{"https": 0} + test.IPAddresses = []string{"888.888.888.888"} + ua.serviceRegistry = map[int]forms.ServiceRecord_v1{0: test} + return ua, nil +} + +type getUniqueSystemsParams struct { + expectError bool + setup func() (ua *UnitAsset, err error) + testCase string +} + +func TestGetUniqueSystems(t *testing.T) { + params := []getUniqueSystemsParams{ + { + false, + func() (ua *UnitAsset, err error) { return createServRegistryHttp() }, + "Best case, http", + }, + { + false, + func() (ua *UnitAsset, err error) { return createServRegistryHttps() }, + "Best case, https", + }, + { + false, + func() (ua *UnitAsset, err error) { return createBrokenServRegistry() }, + "Bad case, http/https not found", + }, + } + + for _, c := range params { + ua, err := c.setup() + if err != nil { + t.Errorf("Failed during setup in '%s' with error: %v", c.testCase, err) + } + _, err = getUniqueSystems(ua) + //log.Printf("sys: %+v", sys) + if c.expectError == false && err != nil { + t.Errorf("Failed while getting unique systems in '%s': %v", c.testCase, err) + } + if c.expectError == true && err == nil { + t.Errorf("Expected errors in '%s'", c.testCase) + } + } +} From b8fbb5a21d57e3200c8c2d649cc63db6213b90a8 Mon Sep 17 00:00:00 2001 From: walpat-1 Date: Mon, 14 Jul 2025 18:07:30 +0200 Subject: [PATCH 41/81] Refactored and simplified FilterByServiceDefinitionAndDetails() --- esr/thing.go | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/esr/thing.go b/esr/thing.go index c8e8888..7e8dca4 100644 --- a/esr/thing.go +++ b/esr/thing.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "log" + "slices" "strconv" "sync" "sync/atomic" @@ -321,6 +322,15 @@ func (ua *UnitAsset) serviceRegistryHandler() { } } +func compareDetails(reqDetails []string, availDetails []string) bool { + for _, requiredValue := range reqDetails { + if slices.Contains(availDetails, requiredValue) { + return true + } + } + return false +} + // FilterByServiceDefinitionAndDetails returns a list of services with the given service definition and details TODO: protocols func (ua *UnitAsset) FilterByServiceDefinitionAndDetails(desiredDefinition string, requiredDetails map[string][]string) []forms.ServiceRecord_v1 { ua.mu.Lock() // Ensure thread safety @@ -341,20 +351,7 @@ func (ua *UnitAsset) FilterByServiceDefinitionAndDetails(desiredDefinition strin } // Ensure at least one value in requiredDetails matches record.Details - valueMatch := false - for _, requiredValue := range values { - for _, recordValue := range recordValues { - if recordValue == requiredValue { - valueMatch = true - break - } - } - if valueMatch { - break - } - } - - if !valueMatch { + if !compareDetails(values, recordValues) { matchesAllDetails = false break } From 7a4b27cc71d8a152e8dc5bd7c0900ed72f4ae45f Mon Sep 17 00:00:00 2001 From: walpat-1 Date: Mon, 14 Jul 2025 18:10:03 +0200 Subject: [PATCH 42/81] Added help functions etc for testing FilterByServiceDefinitionAndDetails() --- esr/thing_test.go | 99 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 95 insertions(+), 4 deletions(-) diff --git a/esr/thing_test.go b/esr/thing_test.go index c0a4f69..199818c 100644 --- a/esr/thing_test.go +++ b/esr/thing_test.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "log" "testing" "github.com/sdoque/mbaigo/forms" @@ -42,11 +43,101 @@ func TestUnmarshalTraits(t *testing.T) {} // (ua *UnitAsset) serviceRegistryHandler() {} func TestServiceRegistryHandler(t *testing.T) {} -// (ua *UnitAsset) FilterByServiceDefinitionAndDetails(desiredDefinition string, requiredDetails map[string][]string) []forms.ServiceRecord_v1 { -func TestFilterByServiceDefAndDetails(t *testing.T) {} +// ------------------------------------------------------------------------ // +// Help functions and structs to test FilterByServiceDefinitionAndDetails() +// ------------------------------------------------------------------------ // -// checkExpiration(ua *UnitAsset, servId int) {} -func TestCheckExpiration(t *testing.T) {} +func createAssetWithServices() (ua *UnitAsset, err error) { + initTemp := initTemplate() + ua, ok := initTemp.(*UnitAsset) + if !ok { + return nil, fmt.Errorf("Failed while typecasting to local UnitAsset") + } + locations := []string{"Kitchen", "Bathroom"} + ua.serviceRegistry = make(map[int]forms.ServiceRecord_v1) + for i, l := range locations { + var form forms.ServiceRecord_v1 + form.ServiceDefinition = "testDef" + form.SystemName = fmt.Sprintf("testSystem%d", i) + form.ProtoPort = map[string]int{"http": i} + form.IPAddresses = []string{fmt.Sprintf("999.999.%d.999", i)} + form.EndOfValidity = "2026-01-02T15:04:05Z" + form.Details = make(map[string][]string) + form.Details = map[string][]string{"Location": {l}} + ua.serviceRegistry[i] = form + } + return ua, nil +} + +func TestFilterByServiceDefAndDetails(t *testing.T) { + ua, err := createAssetWithServices() + if err != nil { + t.Errorf("failed while creating asset at startup") + } + a := map[string][]string{"Location": {"Bathroom"}} + lst := ua.FilterByServiceDefinitionAndDetails("testDef", a) + log.Printf("LST: %+v", lst) +} + +// ---------------------------------------------------- // +// Help functions and structs to test checkExpiration() +// ---------------------------------------------------- // + +func createAssetWithService(year any) (ua *UnitAsset, err error) { + initTemp := initTemplate() + ua, ok := initTemp.(*UnitAsset) + if !ok { + return nil, fmt.Errorf("Failed while typecasting to local UnitAsset") + } + + var test forms.ServiceRecord_v1 + test.SystemName = "testSystem" + test.ProtoPort = map[string]int{"http": 1234} + test.IPAddresses = []string{"999.999.999.999"} + test.EndOfValidity = fmt.Sprintf("%v-01-02T15:04:05Z", year) + ua.serviceRegistry = map[int]forms.ServiceRecord_v1{0: test} + return ua, nil +} + +type checkExpirationParams struct { + servicePresent bool + setup func() (*UnitAsset, error) + testCase string +} + +func TestCheckExpiration(t *testing.T) { + params := []checkExpirationParams{ + { + true, + func() (ua *UnitAsset, err error) { return createAssetWithService(2026) }, + "Best case, service not past expiration", + }, + { + false, + func() (ua *UnitAsset, err error) { return createAssetWithService(2006) }, + "Bad case, service past expiration", + }, + { + true, + func() (ua *UnitAsset, err error) { return createAssetWithService("faulty") }, + "Bad case, time parsing problem", + }, + } + for _, c := range params { + ua, err := c.setup() + if err != nil { + t.Errorf("failed during setup: %v", err) + } + + checkExpiration(ua, 0) + if _, exists := ua.serviceRegistry[0]; (exists == false) && (c.servicePresent == true) { + t.Errorf("expected the service to be present in '%s'", c.testCase) + } + if _, exists := ua.serviceRegistry[0]; (exists == true) && (c.servicePresent == false) { + t.Errorf("expected service to be removed in '%s'", c.testCase) + } + } +} // ----------------------------------------------------- // // Help functions and structs to test getUniqueSystems() From 6d93b1bcaa39e2cac13e1562f3d0d7148bfc8c24 Mon Sep 17 00:00:00 2001 From: Pake Date: Tue, 15 Jul 2025 10:54:38 +0200 Subject: [PATCH 43/81] Finished test for FilterByServiceDefinitionAndDetails() --- esr/thing_test.go | 61 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/esr/thing_test.go b/esr/thing_test.go index 199818c..0c61876 100644 --- a/esr/thing_test.go +++ b/esr/thing_test.go @@ -2,7 +2,6 @@ package main import ( "fmt" - "log" "testing" "github.com/sdoque/mbaigo/forms" @@ -47,15 +46,18 @@ func TestServiceRegistryHandler(t *testing.T) {} // Help functions and structs to test FilterByServiceDefinitionAndDetails() // ------------------------------------------------------------------------ // -func createAssetWithServices() (ua *UnitAsset, err error) { +// Creates an asset multiple services in its registry +func createRegistryWithServices(broken bool) (ua *UnitAsset, err error) { initTemp := initTemplate() ua, ok := initTemp.(*UnitAsset) if !ok { return nil, fmt.Errorf("Failed while typecasting to local UnitAsset") } - locations := []string{"Kitchen", "Bathroom"} + var locations []string + locations = []string{"Kitchen", "Bathroom", "Livingroom"} + ua.serviceRegistry = make(map[int]forms.ServiceRecord_v1) - for i, l := range locations { + for i, location := range locations { var form forms.ServiceRecord_v1 form.ServiceDefinition = "testDef" form.SystemName = fmt.Sprintf("testSystem%d", i) @@ -63,27 +65,55 @@ func createAssetWithServices() (ua *UnitAsset, err error) { form.IPAddresses = []string{fmt.Sprintf("999.999.%d.999", i)} form.EndOfValidity = "2026-01-02T15:04:05Z" form.Details = make(map[string][]string) - form.Details = map[string][]string{"Location": {l}} + if !broken { + form.Details = map[string][]string{"Location": {location}} + } ua.serviceRegistry[i] = form } return ua, nil } +type filterByServDefAndDetailsParams struct { + expectMatch bool + setup func() (*UnitAsset, error) + testCase string +} + func TestFilterByServiceDefAndDetails(t *testing.T) { - ua, err := createAssetWithServices() - if err != nil { - t.Errorf("failed while creating asset at startup") + params := []filterByServDefAndDetailsParams{ + { + true, + func() (ua *UnitAsset, err error) { return createRegistryWithServices(false) }, + "Best case", + }, + { + false, + func() (ua *UnitAsset, err error) { return createRegistryWithServices(true) }, + "Bad case, key doesn't exist", + }, + } + + for _, c := range params { + ua, err := c.setup() + if err != nil { + t.Errorf("Failed during setup in '%s'", c.testCase) + } + checkLoc := map[string][]string{"Location": {"Livingroom"}} + lst := ua.FilterByServiceDefinitionAndDetails("testDef", checkLoc) + if (c.expectMatch == true) && (len(lst) < 1) { + t.Errorf("Expected atleast 1 service") + } + if (c.expectMatch == false) && (len(lst) > 0) { + t.Errorf("Expected no matches") + } } - a := map[string][]string{"Location": {"Bathroom"}} - lst := ua.FilterByServiceDefinitionAndDetails("testDef", a) - log.Printf("LST: %+v", lst) } // ---------------------------------------------------- // // Help functions and structs to test checkExpiration() // ---------------------------------------------------- // -func createAssetWithService(year any) (ua *UnitAsset, err error) { +func createRegistryWithService(year any) (ua *UnitAsset, err error) { initTemp := initTemplate() ua, ok := initTemp.(*UnitAsset) if !ok { @@ -109,17 +139,17 @@ func TestCheckExpiration(t *testing.T) { params := []checkExpirationParams{ { true, - func() (ua *UnitAsset, err error) { return createAssetWithService(2026) }, + func() (ua *UnitAsset, err error) { return createRegistryWithService(2026) }, "Best case, service not past expiration", }, { false, - func() (ua *UnitAsset, err error) { return createAssetWithService(2006) }, + func() (ua *UnitAsset, err error) { return createRegistryWithService(2006) }, "Bad case, service past expiration", }, { true, - func() (ua *UnitAsset, err error) { return createAssetWithService("faulty") }, + func() (ua *UnitAsset, err error) { return createRegistryWithService("faulty") }, "Bad case, time parsing problem", }, } @@ -221,7 +251,6 @@ func TestGetUniqueSystems(t *testing.T) { t.Errorf("Failed during setup in '%s' with error: %v", c.testCase, err) } _, err = getUniqueSystems(ua) - //log.Printf("sys: %+v", sys) if c.expectError == false && err != nil { t.Errorf("Failed while getting unique systems in '%s': %v", c.testCase, err) } From 15c9cd8242efe8a21e78d10e35914a25aa0efd48 Mon Sep 17 00:00:00 2001 From: walpat-1 Date: Tue, 15 Jul 2025 14:30:48 +0200 Subject: [PATCH 44/81] Added test for newResource --- esr/thing_test.go | 91 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 82 insertions(+), 9 deletions(-) diff --git a/esr/thing_test.go b/esr/thing_test.go index 0c61876..745124f 100644 --- a/esr/thing_test.go +++ b/esr/thing_test.go @@ -1,10 +1,14 @@ package main import ( + "context" + "encoding/json" "fmt" "testing" + "github.com/sdoque/mbaigo/components" "github.com/sdoque/mbaigo/forms" + "github.com/sdoque/mbaigo/usecases" ) // ----------------------------------------------------- // @@ -29,18 +33,87 @@ func TestInitTemplate(t *testing.T) { } } -// --------------------------------------------- // -// Help functions and structs to test *** -// --------------------------------------------- // +// ------------------------------------------------ // +// Help functions and structs to test newResource() +// ------------------------------------------------ // -// newResource(configuredAsset usecases.ConfigurableAsset, sys *components.System) (components.UnitAsset, func()) {...} -func TestNewResource(t *testing.T) {} +// Create a error reader to break json.Unmarshal() +type errReader int -// UnmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) {} -func TestUnmarshalTraits(t *testing.T) {} +var errBodyRead error = fmt.Errorf("bad body read") -// (ua *UnitAsset) serviceRegistryHandler() {} -func TestServiceRegistryHandler(t *testing.T) {} +func (errReader) Read(p []byte) (n int, err error) { + return 0, errBodyRead +} +func (errReader) Close() error { + return nil +} + +func createConfAssetBrokenTraits() usecases.ConfigurableAsset { + brokenTrait, _ := json.Marshal(errReader(0)) + uac := usecases.ConfigurableAsset{ + Name: "testRegistrar", + Details: map[string][]string{"testDetail": {"detail1", "detail2"}}, + Services: []components.Service{}, + Traits: []json.RawMessage{json.RawMessage(brokenTrait)}, + } + return uac +} + +func createConfAssetMultipleTraits() usecases.ConfigurableAsset { + uac := usecases.ConfigurableAsset{ + Name: "testRegistrar", + Details: map[string][]string{"testDetail": {"detail1", "detail2"}}, + Services: []components.Service{}, + Traits: []json.RawMessage{json.RawMessage(`{"recCount": 0}`), json.RawMessage(`{"leading": false}`)}, + } + return uac +} + +func createTestSystem() components.System { + ctx := context.Background() + sys := components.NewSystem("testsys", ctx) + sys.Husk = &components.Husk{ + Description: " is for testing purposes", + Certificate: "ABCD", + Details: map[string][]string{"Developer": {"Arrowhead"}}, + ProtoPort: map[string]int{"https": 0, "http": 8870, "coap": 0}, + InfoLink: "https://for.testing.purposes", + } + return sys +} + +type newResourceParams struct { + setup func() components.System + confAsset func() usecases.ConfigurableAsset + testCase string +} + +func TestNewResource(t *testing.T) { + params := []newResourceParams{ + { + func() (sys components.System) { return createTestSystem() }, + func() (confAsset usecases.ConfigurableAsset) { return createConfAssetBrokenTraits() }, + "Case: unmarshal traits fails", + }, + { + func() (sys components.System) { return createTestSystem() }, + func() (confAsset usecases.ConfigurableAsset) { return createConfAssetMultipleTraits() }, + "Case: confAsset has multiple traits", + }, + } + + for _, c := range params { + sys := c.setup() + uac := c.confAsset() + + ua, shutdown := newResource(uac, &sys) + shutdown() + if ua.GetName() != "testRegistrar" { + t.Errorf("Name mismatch, expected '%s' got '%s'", uac.Name, ua.GetName()) + } + } +} // ------------------------------------------------------------------------ // // Help functions and structs to test FilterByServiceDefinitionAndDetails() From c202f193b56d817182796d0c0051baf2a4bb1217 Mon Sep 17 00:00:00 2001 From: walpat-1 Date: Tue, 15 Jul 2025 17:23:15 +0200 Subject: [PATCH 45/81] Added CoreS to createTestSystem() --- esr/thing_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/esr/thing_test.go b/esr/thing_test.go index 745124f..4b3930c 100644 --- a/esr/thing_test.go +++ b/esr/thing_test.go @@ -80,6 +80,18 @@ func createTestSystem() components.System { ProtoPort: map[string]int{"https": 0, "http": 8870, "coap": 0}, InfoLink: "https://for.testing.purposes", } + leadingRegistrar := &components.CoreSystem{ + Name: components.ServiceRegistrarName, + Url: "https://leadingregistrar:1234", + } + orchestrator := &components.CoreSystem{ + Name: "orchestrator", + Url: "https://orchestator:1234", + } + sys.CoreS = []*components.CoreSystem{ + leadingRegistrar, + orchestrator, + } return sys } From 0acfb7c4576a829891ef53d15c92342a98df0e6e Mon Sep 17 00:00:00 2001 From: walpat-1 Date: Tue, 15 Jul 2025 17:28:02 +0200 Subject: [PATCH 46/81] Added tests for roleStatus() and peersList() --- esr/esr_test.go | 185 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 esr/esr_test.go diff --git a/esr/esr_test.go b/esr/esr_test.go new file mode 100644 index 0000000..50e4d28 --- /dev/null +++ b/esr/esr_test.go @@ -0,0 +1,185 @@ +package main + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/sdoque/mbaigo/components" +) + +// ----------------------------------------------- // +// Help functions and structs to test roleStatus() +// ----------------------------------------------- // + +func createLeadingRegistrar() UnitAsset { + uac := UnitAsset{ + Name: "testRegistrar", + Details: map[string][]string{"testDetail": {"detail1", "detail2"}}, + ServicesMap: components.Services{}, + Traits: Traits{ + leading: true, + leadingSince: time.Now(), + }, + } + return uac +} + +func createNonLeadingRegistrar() UnitAsset { + uac := UnitAsset{ + Name: "testRegistrar", + Details: map[string][]string{"testDetail": {"detail1", "detail2"}}, + ServicesMap: components.Services{}, + Traits: Traits{ + leading: false, + leadingRegistrar: &components.CoreSystem{Name: "otherRegistrar", Url: "otherURL"}, + }, + } + return uac +} + +func createServiceUnavailableRegistrar() UnitAsset { + uac := UnitAsset{ + Name: "testRegistrar", + Details: map[string][]string{"testDetail": {"detail1", "detail2"}}, + ServicesMap: components.Services{}, + Traits: Traits{ + leading: false, + leadingRegistrar: nil, + }, + } + return uac +} + +type roleStatusParams struct { + expectedStatuscode int + setup func() UnitAsset + request *http.Request + testCase string +} + +func TestRoleStatus(t *testing.T) { + params := []roleStatusParams{ + { + 200, + func() UnitAsset { return createLeadingRegistrar() }, + httptest.NewRequest(http.MethodGet, "http://localhost/test", nil), + "Good case, leading registrar", + }, + { + 503, + func() UnitAsset { return createNonLeadingRegistrar() }, + httptest.NewRequest(http.MethodGet, "http://localhost/test", nil), + "Good case, leading registrar", + }, + { + 503, + func() UnitAsset { return createServiceUnavailableRegistrar() }, + httptest.NewRequest(http.MethodGet, "http://localhost/test", nil), + "Bad case, service unavailable", + }, + { + 200, + func() UnitAsset { return UnitAsset{} }, + httptest.NewRequest(http.MethodPost, "http://localhost/test", nil), + "Bad case, unsupported http method", + }, + } + for _, c := range params { + ua := c.setup() + w := httptest.NewRecorder() + r := c.request + + ua.roleStatus(w, r) + statusCode := w.Result().StatusCode + if statusCode != c.expectedStatuscode { + t.Errorf("Failed '%s', expected statuscode %d got: %d", c.testCase, c.expectedStatuscode, statusCode) + } + } +} + +// ---------------------------------------------- // +// Help functions and structs to test peersList() +// ---------------------------------------------- // + +func createTestSysMultipleRegistrars(port string) components.System { + sys := createTestSystem() + sys.CoreS = []*components.CoreSystem{} + for num := range 5 { + reg := &components.CoreSystem{ + Name: "serviceregistrar", + Url: fmt.Sprintf("http://localhost:%s/%d", port, num), + } + sys.CoreS = append(sys.CoreS, reg) + } + return sys +} + +func createTestSysBrokenRegistrarURL() components.System { + sys := createTestSystem() + sys.CoreS = []*components.CoreSystem{} + + reg := &components.CoreSystem{ + Name: "serviceregistrar", + Url: string(rune(0)), + } + sys.CoreS = append(sys.CoreS, reg) + + return sys +} + +type peersListParams struct { + expectError bool + setup func() components.System + testCase string +} + +func TestPeersList(t *testing.T) { + params := []peersListParams{ + { + false, + func() (sys components.System) { return createTestSystem() }, + "Good case, one registrar", + }, + { + false, + func() (sys components.System) { return createTestSysMultipleRegistrars("1234") }, + "Good case, multiple registrars", + }, + { + false, + func() (sys components.System) { return createTestSysMultipleRegistrars("") }, + "Bad case, port missing", + }, + { + false, + func() (sys components.System) { return createTestSysMultipleRegistrars("8870") }, + "Bad case, port same as http in husk", + }, + { + true, + func() (sys components.System) { return createTestSysBrokenRegistrarURL() }, + "Bad case, can't parse url", + }, + } + + for _, c := range params { + sys := c.setup() + _, err := peersList(&sys) + if (c.expectError == false) && (err != nil) { + t.Errorf("Expected no errors in '%s', got: %v", c.testCase, err) + } + if (c.expectError == true) && (err == nil) { + t.Errorf("Expected errors in '%s'", c.testCase) + } + } + +} + +// ----------------------------------------------- // +// Help functions and structs to test systemList() +// ----------------------------------------------- // + +// func (ua *UnitAsset) systemList(w http.ResponseWriter, r *http.Request) { From 05f8581aef472fe38681b147e49488c64dc19122 Mon Sep 17 00:00:00 2001 From: walpat-1 Date: Tue, 15 Jul 2025 19:13:13 +0200 Subject: [PATCH 47/81] Added tests for systemList() --- esr/esr_test.go | 76 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/esr/esr_test.go b/esr/esr_test.go index 50e4d28..da8535c 100644 --- a/esr/esr_test.go +++ b/esr/esr_test.go @@ -1,13 +1,16 @@ package main import ( + "encoding/json" "fmt" + "io" "net/http" "net/http/httptest" "testing" "time" "github.com/sdoque/mbaigo/components" + "github.com/sdoque/mbaigo/forms" ) // ----------------------------------------------- // @@ -175,11 +178,80 @@ func TestPeersList(t *testing.T) { t.Errorf("Expected errors in '%s'", c.testCase) } } - } // ----------------------------------------------- // // Help functions and structs to test systemList() // ----------------------------------------------- // -// func (ua *UnitAsset) systemList(w http.ResponseWriter, r *http.Request) { +func createFilledRegistrar() UnitAsset { + ua := createLeadingRegistrar() + ua.serviceRegistry = make(map[int]forms.ServiceRecord_v1) + var serviceAmount int + for x := range 5 { + serviceAmount++ + ua.serviceRegistry[x] = forms.ServiceRecord_v1{Id: x, SystemName: fmt.Sprintf("testSys%d", x), + IPAddresses: []string{"localhost"}, ProtoPort: map[string]int{"http": 1234}} + } + return ua +} + +type expectedBody struct { + List []string `json:"systemurl"` + Version string `json:"version"` +} + +type systemListParams struct { + expectedStatuscode int + setup func() UnitAsset + request *http.Request + testCase string +} + +func TestSystemList(t *testing.T) { + params := []systemListParams{ + { + 200, + func() UnitAsset { return createFilledRegistrar() }, + httptest.NewRequest(http.MethodGet, "http://localhost", nil), + "Best case", + }, + { + 405, + func() UnitAsset { return createFilledRegistrar() }, + httptest.NewRequest(http.MethodPost, "http://localhost", nil), + "Bad case, unsupported http method", + }, + } + + for _, c := range params { + ua := c.setup() + w := httptest.NewRecorder() + r := c.request + + ua.systemList(w, r) + res := w.Result() + data, err := io.ReadAll(res.Body) + if err != nil { + t.Errorf("Failed while reading response body") + } + + var jsonData expectedBody + // Only unmarshal the data if it's a successful request + if res.StatusCode == 200 { + err = json.Unmarshal(data, &jsonData) + if err != nil { + t.Errorf("Failed while unmarshalling data") + } + } + + if (res.StatusCode == 200) && (len(jsonData.List) != 5) { + t.Errorf("Expected status code '%d' got '%d', and length of list '%d' got '%d'", + c.expectedStatuscode, res.StatusCode, 5, len(jsonData.List)) + } + + if c.expectedStatuscode == 405 && res.Status != "405 Method Not Allowed" { + t.Errorf("Expected '405 Method Not Allowed' as Status, got: %v", res.Status) + } + } +} From 96d8109ffab28e43874d1856019a5b25045b2860 Mon Sep 17 00:00:00 2001 From: walpat-1 Date: Wed, 16 Jul 2025 14:18:22 +0200 Subject: [PATCH 48/81] Changed helpfunctions returned unitasset to pointer --- esr/esr_test.go | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/esr/esr_test.go b/esr/esr_test.go index da8535c..61d2cbb 100644 --- a/esr/esr_test.go +++ b/esr/esr_test.go @@ -17,8 +17,8 @@ import ( // Help functions and structs to test roleStatus() // ----------------------------------------------- // -func createLeadingRegistrar() UnitAsset { - uac := UnitAsset{ +func createLeadingRegistrar() *UnitAsset { + uac := &UnitAsset{ Name: "testRegistrar", Details: map[string][]string{"testDetail": {"detail1", "detail2"}}, ServicesMap: components.Services{}, @@ -30,8 +30,8 @@ func createLeadingRegistrar() UnitAsset { return uac } -func createNonLeadingRegistrar() UnitAsset { - uac := UnitAsset{ +func createNonLeadingRegistrar() *UnitAsset { + uac := &UnitAsset{ Name: "testRegistrar", Details: map[string][]string{"testDetail": {"detail1", "detail2"}}, ServicesMap: components.Services{}, @@ -43,8 +43,8 @@ func createNonLeadingRegistrar() UnitAsset { return uac } -func createServiceUnavailableRegistrar() UnitAsset { - uac := UnitAsset{ +func createServiceUnavailableRegistrar() *UnitAsset { + uac := &UnitAsset{ Name: "testRegistrar", Details: map[string][]string{"testDetail": {"detail1", "detail2"}}, ServicesMap: components.Services{}, @@ -58,7 +58,7 @@ func createServiceUnavailableRegistrar() UnitAsset { type roleStatusParams struct { expectedStatuscode int - setup func() UnitAsset + setup func() *UnitAsset request *http.Request testCase string } @@ -67,25 +67,25 @@ func TestRoleStatus(t *testing.T) { params := []roleStatusParams{ { 200, - func() UnitAsset { return createLeadingRegistrar() }, + func() *UnitAsset { return createLeadingRegistrar() }, httptest.NewRequest(http.MethodGet, "http://localhost/test", nil), "Good case, leading registrar", }, { 503, - func() UnitAsset { return createNonLeadingRegistrar() }, + func() *UnitAsset { return createNonLeadingRegistrar() }, httptest.NewRequest(http.MethodGet, "http://localhost/test", nil), "Good case, leading registrar", }, { 503, - func() UnitAsset { return createServiceUnavailableRegistrar() }, + func() *UnitAsset { return createServiceUnavailableRegistrar() }, httptest.NewRequest(http.MethodGet, "http://localhost/test", nil), "Bad case, service unavailable", }, { 200, - func() UnitAsset { return UnitAsset{} }, + func() *UnitAsset { return &UnitAsset{} }, httptest.NewRequest(http.MethodPost, "http://localhost/test", nil), "Bad case, unsupported http method", }, @@ -184,7 +184,7 @@ func TestPeersList(t *testing.T) { // Help functions and structs to test systemList() // ----------------------------------------------- // -func createFilledRegistrar() UnitAsset { +func createFilledRegistrar() *UnitAsset { ua := createLeadingRegistrar() ua.serviceRegistry = make(map[int]forms.ServiceRecord_v1) var serviceAmount int @@ -203,7 +203,7 @@ type expectedBody struct { type systemListParams struct { expectedStatuscode int - setup func() UnitAsset + setup func() *UnitAsset request *http.Request testCase string } @@ -212,13 +212,13 @@ func TestSystemList(t *testing.T) { params := []systemListParams{ { 200, - func() UnitAsset { return createFilledRegistrar() }, + func() *UnitAsset { return createFilledRegistrar() }, httptest.NewRequest(http.MethodGet, "http://localhost", nil), "Best case", }, { 405, - func() UnitAsset { return createFilledRegistrar() }, + func() *UnitAsset { return createFilledRegistrar() }, httptest.NewRequest(http.MethodPost, "http://localhost", nil), "Bad case, unsupported http method", }, @@ -246,8 +246,8 @@ func TestSystemList(t *testing.T) { } if (res.StatusCode == 200) && (len(jsonData.List) != 5) { - t.Errorf("Expected status code '%d' got '%d', and length of list '%d' got '%d'", - c.expectedStatuscode, res.StatusCode, 5, len(jsonData.List)) + t.Errorf("Expected status code '%d' and length of list '%d' got: '%d' and '%d'", + c.expectedStatuscode, 5, res.StatusCode, len(jsonData.List)) } if c.expectedStatuscode == 405 && res.Status != "405 Method Not Allowed" { From 4741f9fd4b1a282b66e7bbb8d909bc8b98bb72f6 Mon Sep 17 00:00:00 2001 From: walpat-1 Date: Wed, 16 Jul 2025 15:40:32 +0200 Subject: [PATCH 49/81] Fixed linter errors: added error handlers and var declaration --- esr/esr.go | 24 ++++++++++++++++++------ esr/thing_test.go | 4 ++-- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/esr/esr.go b/esr/esr.go index 9b62f0b..b5963d0 100644 --- a/esr/esr.go +++ b/esr/esr.go @@ -120,7 +120,9 @@ func (ua *UnitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath func (ua *UnitAsset) updateDB(w http.ResponseWriter, r *http.Request) { if !ua.leading { w.WriteHeader(http.StatusServiceUnavailable) - w.Write([]byte("Service Unavailable")) + if _, err := w.Write([]byte("Service Unavailable")); err != nil { + log.Printf("error occured while writing to responsewriter: %v", err) + } return } switch r.Method { @@ -203,9 +205,13 @@ func (ua *UnitAsset) queryDB(w http.ResponseWriter, r *http.Request) { case servvicesList := <-recordsRequest.Result: // Build the HTML response text := "" - w.Write([]byte(text)) + if _, err := w.Write([]byte(text)); err != nil { + log.Printf("error occured while writing to responsewriter: %v", err) + } text = "

    The local cloud's currently available services are:

      " - w.Write([]byte(text)) + if _, err := w.Write([]byte(text)); err != nil { + log.Printf("error occured while writing to responsewriter: %v", err) + } for _, servRec := range servvicesList { metaservice := "" for key, values := range servRec.Details { @@ -215,10 +221,14 @@ func (ua *UnitAsset) queryDB(w http.ResponseWriter, r *http.Request) { parts := strings.Split(servRec.SubPath, "/") uaName := parts[0] sLine := "

      Service ID: " + strconv.Itoa(int(servRec.Id)) + " with definition " + servRec.ServiceDefinition + " from the " + servRec.SystemName + "/" + uaName + " with details " + metaservice + " will expire at: " + servRec.EndOfValidity + "

      " - w.Write([]byte(fmt.Sprintf("
    • %s
    • ", sLine))) + if _, err := w.Write([]byte(fmt.Sprintf("
    • %s
    • ", sLine))); err != nil { + log.Printf("error occured while writing to responsewriter: %v", err) + } } text = "
    " - w.Write([]byte(text)) + if _, err := w.Write([]byte(text)); err != nil { + log.Printf("error occured while writing to responsewriter: %v", err) + } case <-time.After(5 * time.Second): // Optional timeout http.Error(w, "Request timed out", http.StatusGatewayTimeout) log.Println("Failure to process service listing request") @@ -339,7 +349,9 @@ func (ua *UnitAsset) roleStatus(w http.ResponseWriter, r *http.Request) { return } w.WriteHeader(http.StatusServiceUnavailable) - w.Write([]byte("Service Unavailable")) + if _, err := w.Write([]byte("Service Unavailable")); err != nil { + log.Printf("error occured while writing to responsewriter: %v", err) + } default: fmt.Fprintf(w, "unsupported http request method") } diff --git a/esr/thing_test.go b/esr/thing_test.go index 4b3930c..30a1ae7 100644 --- a/esr/thing_test.go +++ b/esr/thing_test.go @@ -138,8 +138,8 @@ func createRegistryWithServices(broken bool) (ua *UnitAsset, err error) { if !ok { return nil, fmt.Errorf("Failed while typecasting to local UnitAsset") } - var locations []string - locations = []string{"Kitchen", "Bathroom", "Livingroom"} + + var locations = []string{"Kitchen", "Bathroom", "Livingroom"} ua.serviceRegistry = make(map[int]forms.ServiceRecord_v1) for i, location := range locations { From c981f21a8096e9d210b70fb5405f88a69101a385 Mon Sep 17 00:00:00 2001 From: walpat-1 Date: Wed, 16 Jul 2025 17:52:27 +0200 Subject: [PATCH 50/81] added missing mutex locks & unlocks --- esr/thing.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esr/thing.go b/esr/thing.go index 7e8dca4..1ad0591 100644 --- a/esr/thing.go +++ b/esr/thing.go @@ -312,11 +312,13 @@ func (ua *UnitAsset) serviceRegistryHandler() { case "delete": // Handle delete record + ua.mu.Lock() ua.sched.RemoveTask(int(request.Id)) delete(ua.serviceRegistry, int(request.Id)) if _, exists := ua.serviceRegistry[int(request.Id)]; !exists { log.Printf("The service with ID %d has been deleted.", request.Id) } + ua.mu.Unlock() request.Error <- nil // Send success response } } @@ -368,6 +370,8 @@ func (ua *UnitAsset) FilterByServiceDefinitionAndDetails(desiredDefinition strin // checkExpiration checks if a service has expired and deletes it if it has. func checkExpiration(ua *UnitAsset, servId int) { + ua.mu.Lock() + defer ua.mu.Unlock() dbRec := ua.serviceRegistry[servId] expiration, err := time.Parse(time.RFC3339, dbRec.EndOfValidity) if err != nil { From 4d930babdafedb40387e7417e62c45ef433f5120 Mon Sep 17 00:00:00 2001 From: walpat-1 Date: Wed, 16 Jul 2025 17:52:48 +0200 Subject: [PATCH 51/81] added first test for serviceRegistryHandler() --- esr/thing_test.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/esr/thing_test.go b/esr/thing_test.go index 30a1ae7..a70ed6c 100644 --- a/esr/thing_test.go +++ b/esr/thing_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "testing" + "time" "github.com/sdoque/mbaigo/components" "github.com/sdoque/mbaigo/forms" @@ -127,6 +128,36 @@ func TestNewResource(t *testing.T) { } } +// ----------------------------------------------------------- // +// Help functions and structs to test serviceRegistryHandler() +// ----------------------------------------------------------- // + +func sendAddRequests(num int64, ua *UnitAsset, shutdown func()) { + for x := range num { + ua.mu.Lock() + ua.requests <- ServiceRegistryRequest{ + Action: "add", + Record: &forms.ServiceRecord_v1{Id: int(x), ServiceDefinition: fmt.Sprintf("Service%d", x), + EndOfValidity: fmt.Sprintf("%v", time.Now().Add(1*time.Hour))}, + Id: 0, + } + ua.mu.Unlock() + } +} + +func TestServiceRegistryHandler(t *testing.T) { + // Setup + temp := createConfAssetMultipleTraits() + sys := createTestSystem() + res, shutdown := newResource(temp, &sys) + ua, _ := res.(*UnitAsset) + time.Sleep(1 * time.Second) + sendAddRequests(1, ua, shutdown) + time.Sleep(1 * time.Second) + shutdown() + time.Sleep(1 * time.Second) +} + // ------------------------------------------------------------------------ // // Help functions and structs to test FilterByServiceDefinitionAndDetails() // ------------------------------------------------------------------------ // From b84651a59111de95aeef2f993cfefc05eaf5c439 Mon Sep 17 00:00:00 2001 From: Pake Date: Thu, 17 Jul 2025 19:12:54 +0200 Subject: [PATCH 52/81] Added tests for serviceRegistryHandler() --- esr/thing_test.go | 369 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 345 insertions(+), 24 deletions(-) diff --git a/esr/thing_test.go b/esr/thing_test.go index a70ed6c..e690164 100644 --- a/esr/thing_test.go +++ b/esr/thing_test.go @@ -2,6 +2,7 @@ package main import ( "context" + "crypto/x509/pkix" "encoding/json" "fmt" "testing" @@ -128,34 +129,354 @@ func TestNewResource(t *testing.T) { } } -// ----------------------------------------------------------- // -// Help functions and structs to test serviceRegistryHandler() -// ----------------------------------------------------------- // - -func sendAddRequests(num int64, ua *UnitAsset, shutdown func()) { - for x := range num { - ua.mu.Lock() - ua.requests <- ServiceRegistryRequest{ - Action: "add", - Record: &forms.ServiceRecord_v1{Id: int(x), ServiceDefinition: fmt.Sprintf("Service%d", x), - EndOfValidity: fmt.Sprintf("%v", time.Now().Add(1*time.Hour))}, - Id: 0, +// --------------------------------------------------------------------------- // +// Help functions and structs to test the add part of serviceRegistryHandler() +// --------------------------------------------------------------------------- // + +func createNewSys() components.System { + // prepare for graceful shutdown + ctx, cancel := context.WithCancel(context.Background()) // create a context that can be cancelled + defer cancel() // make sure all paths cancel the context to avoid context leak + + // instantiate the System + sys := components.NewSystem("serviceregistrar", ctx) + + // Instantiate the Capsule + sys.Husk = &components.Husk{ + Description: "is an Arrowhead mandatory core system that keeps track of the currently available services.", + Details: map[string][]string{"Developer": {"Synecdoque"}}, + ProtoPort: map[string]int{"https": 0, "http": 20102, "coap": 0}, + InfoLink: "https://github.com/sdoque/systems/tree/main/esr", + DName: pkix.Name{ + CommonName: sys.Name, + Organization: []string{"Synecdoque"}, + OrganizationalUnit: []string{"Systems"}, + Locality: []string{"Luleå"}, + Province: []string{"Norrbotten"}, + Country: []string{"SE"}, + }, + } + + // instantiate a template unit asset + assetTemplate := initTemplate() + assetName := assetTemplate.GetName() + sys.UAssets[assetName] = &assetTemplate + return sys +} + +func sendAddRequest(id int64, def string, subPath string, created string, ch chan ServiceRegistryRequest) error { + rec := &forms.ServiceRecord_v1{ + Id: int(id), + ServiceDefinition: def, + SystemName: "System", + ServiceNode: "node", + IPAddresses: []string{"123.456.789.012"}, + ProtoPort: map[string]int{"http": 1234}, + Details: map[string][]string{"details": {}}, + Certificate: "ABCD", + SubPath: subPath, + RegLife: 25, + Version: "SignalA_v1a", + Created: created, + Updated: time.Now().String(), + EndOfValidity: time.Now().Add(25 * time.Second).String(), + SubscribeAble: false, + ACost: float64(id), + CUnit: "", + } + + req := ServiceRegistryRequest{ + Action: "add", + Record: rec, + Error: make(chan error), + } + + ch <- req + select { + case err := <-req.Error: + return err + } +} + +func sendBrokenAddRequest(num int64, ch chan ServiceRegistryRequest) error { + rec := &forms.SignalA_v1a{} + req := ServiceRegistryRequest{ + Action: "add", + Record: rec, + Id: num, + Error: make(chan error), + } + ch <- req + select { + case err := <-req.Error: + return err + } +} + +type serviceRegistryHandlerParams struct { + expectError bool + request func(*UnitAsset) error + testCase string +} + +func TestServiceRegistryHandlerAdd(t *testing.T) { + params := []serviceRegistryHandlerParams{ + { + false, + func(ua *UnitAsset) error { + return sendAddRequest(0, "testDef", "subP", time.Now().Format(time.RFC3339), ua.requests) + }, + "Best case, successful request", + }, + { + true, + func(ua *UnitAsset) error { return sendBrokenAddRequest(0, ua.requests) }, + "Bad case, unable to convert to correct form", + }, + { + true, + func(ua *UnitAsset) error { + return sendAddRequest(1, "testDef", "subP", time.Now().Format(time.RFC3339), ua.requests) + }, + "Bad case, service doesn't exist in registry", + }, + { + true, + func(ua *UnitAsset) error { + sendAddRequest(0, "testDef", "subP", time.Now().Format(time.RFC3339), ua.requests) + err := sendAddRequest(1, "testDef2", "subP", time.Now().Format(time.RFC3339), ua.requests) + return err + }, + "Bad case, exists with different service definition", + }, + { + true, + func(ua *UnitAsset) error { + sendAddRequest(0, "testDef", "subP", time.Now().Format(time.RFC3339), ua.requests) + err := sendAddRequest(1, "testDef", "subPa", time.Now().Format(time.RFC3339), ua.requests) + return err + }, + "Bad case, exists with different subpath", + }, + { + true, + func(ua *UnitAsset) error { + sendAddRequest(0, "testDef", "subP", time.Now().Format(time.RFC3339), ua.requests) + err := sendAddRequest(1, "testDef", "subP", "", ua.requests) + return err + }, + "Bad case, exists different creation time in updated record", + }, + { + true, + func(ua *UnitAsset) error { + ch := ua.requests + sendAddRequest(0, "testDef", "subP", time.Now().Format(time.RFC3339), ch) + err := sendAddRequest(1, "testDef", "subP", time.Now().Add(1*time.Hour).Format(time.RFC3339), ch) + return err + }, + "Bad case, mismatch between db- and received created field", + }, + { + false, + func(ua *UnitAsset) error { + ch := ua.requests + sendAddRequest(0, "testDef", "subP", time.Now().Format(time.RFC3339), ch) + err := sendAddRequest(0, "testDef", "subP", time.Now().Format(time.RFC3339), ch) + return err + }, + "Bad case, recCount has looped back to 0", + }, + { + false, + func(ua *UnitAsset) error { + ch := ua.requests + sendAddRequest(0, "testDef", "subP", time.Now().Format(time.RFC3339), ch) + err := sendAddRequest(1, "testDef", "subP", time.Now().Format(time.RFC3339), ch) + return err + }, + "Good case, updated db record", + }, + } + + for _, c := range params { + // Setup + temp := createConfAssetMultipleTraits() + sys := createNewSys() + res, shutdown := newResource(temp, &sys) + ua, _ := res.(*UnitAsset) + + // Test and check + err := c.request(ua) + if c.expectError == false && err != nil { + t.Errorf("Expected no errors in '%s': %v", c.testCase, err) } - ua.mu.Unlock() + if c.expectError == true && err == nil { + t.Errorf("Expected errors in '%s'", c.testCase) + } + shutdown() } } -func TestServiceRegistryHandler(t *testing.T) { - // Setup - temp := createConfAssetMultipleTraits() - sys := createTestSystem() - res, shutdown := newResource(temp, &sys) - ua, _ := res.(*UnitAsset) - time.Sleep(1 * time.Second) - sendAddRequests(1, ua, shutdown) - time.Sleep(1 * time.Second) - shutdown() - time.Sleep(1 * time.Second) +// --------------------------------------------------------------------------- // +// Help functions and structs to test the read part of serviceRegistryHandler() +// --------------------------------------------------------------------------- // + +func sendAddRequestWithDetails(id int64, def string, subPath string, created string, ch chan ServiceRegistryRequest) error { + rec := &forms.ServiceRecord_v1{ + Id: 0, + ServiceDefinition: def, + SystemName: "System", + ServiceNode: "node", + IPAddresses: []string{"123.456.789.012"}, + ProtoPort: map[string]int{"http": 1234}, + Details: map[string][]string{"details": {}}, + Certificate: "ABCD", + SubPath: subPath, + RegLife: 25, + Version: "SignalA_v1a", + Created: created, + Updated: time.Now().String(), + EndOfValidity: time.Now().Add(25 * time.Second).String(), + SubscribeAble: false, + ACost: float64(id), + CUnit: "", + } + + for x := range id { + rec.Details["details"] = append(rec.Details["details"], fmt.Sprintf("detail%d", x+1)) + } + + req := ServiceRegistryRequest{ + Action: "add", + Record: rec, + Error: make(chan error), + } + + ch <- req + select { + case err := <-req.Error: + return err + } +} + +// id 0 will return all items in service registry, any other will return items depending on details & definition +func sendReadRequest(id int64, def string, details []string, ch chan ServiceRegistryRequest) ([]forms.ServiceRecord_v1, error) { + rec := &forms.ServiceQuest_v1{ + SysId: 999, + RequesterName: "requester", + ServiceDefinition: def, + Protocol: "", + Details: map[string][]string{"details": details}, + Version: "", + } + var req ServiceRegistryRequest + if id == 0 { + // Returns a specific + req = ServiceRegistryRequest{ + Action: "read", + Record: nil, + Result: make(chan []forms.ServiceRecord_v1), + Error: make(chan error), + } + } else { + // Returns full list of services + req = ServiceRegistryRequest{ + Action: "read", + Record: rec, + Result: make(chan []forms.ServiceRecord_v1), + Error: make(chan error), + } + } + + ch <- req + select { + case err := <-req.Error: + return nil, err + case lst := <-req.Result: + return lst, nil + } +} + +func sendBrokenReadRequest(ch chan ServiceRegistryRequest) ([]forms.ServiceRecord_v1, error) { + rec := &forms.SignalA_v1a{} + var req ServiceRegistryRequest + + req = ServiceRegistryRequest{ + Action: "read", + Record: rec, + Result: make(chan []forms.ServiceRecord_v1), + Error: make(chan error), + } + + ch <- req + select { + case err := <-req.Error: + return nil, err + case lst := <-req.Result: + return lst, nil + } +} + +type serviceRegistryHandlerReadParams struct { + expectError bool + expectedLen int + request func(ua *UnitAsset) ([]forms.ServiceRecord_v1, error) + testCase string +} + +func TestServiceRegistryHandlerRead(t *testing.T) { + params := []serviceRegistryHandlerReadParams{ + { + false, + 1, + func(ua *UnitAsset) ([]forms.ServiceRecord_v1, error) { + return sendReadRequest(0, "", []string{""}, ua.requests) + }, + "Best case, successful read request returning all items", + }, + { + false, + 1, + func(ua *UnitAsset) ([]forms.ServiceRecord_v1, error) { + return sendReadRequest(1, "test", []string{"detail6"}, ua.requests) + }, + "Best case, successful read request returning specific items", + }, + { + true, + 0, + func(ua *UnitAsset) ([]forms.ServiceRecord_v1, error) { + return sendBrokenReadRequest(ua.requests) + }, + "Bad case, wrong form", + }, + } + + for _, c := range params { + // Setup + temp := createConfAssetMultipleTraits() + sys := createNewSys() + res, shutdown := newResource(temp, &sys) + ua, _ := res.(*UnitAsset) + time.Sleep(25 * time.Millisecond) + // Add some services to the serviceregistrar with details: detail1 detail2 ... detailN + sendAddRequestWithDetails(1, "test", "sub1", time.Now().Format(time.RFC3339), ua.requests) + sendAddRequestWithDetails(4, "test", "sub2", time.Now().Format(time.RFC3339), ua.requests) + sendAddRequestWithDetails(8, "test", "sub3", time.Now().Format(time.RFC3339), ua.requests) + + lst, err := c.request(ua) + + if c.expectError == false && err != nil && len(lst) != c.expectedLen { + t.Errorf("Expected no errors in '%s', got: %v, with length of list: %d got %d", + c.testCase, err, c.expectedLen, len(lst)) + } + if c.expectError == true && err == nil { + t.Errorf("Expected errors in '%s'", c.testCase) + } + + shutdown() + } } // ------------------------------------------------------------------------ // From 06e5ddc4a66ee4b1158899fbd093e4e417131f80 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 18 Jul 2025 11:50:37 +0200 Subject: [PATCH 53/81] Adds go.mod files as they are required for Go projects --- esr/go.mod | 8 ++++++++ esr/go.sum | 2 ++ 2 files changed, 10 insertions(+) create mode 100644 esr/go.mod create mode 100644 esr/go.sum diff --git a/esr/go.mod b/esr/go.mod new file mode 100644 index 0000000..83d563f --- /dev/null +++ b/esr/go.mod @@ -0,0 +1,8 @@ +module github.com/sdoque/systems/esr + +go 1.24.4 + +require github.com/sdoque/mbaigo v0.0.0-20250520155324-7390c339652a + +// Replaces this library with a patched version +replace github.com/sdoque/mbaigo v0.0.0-20250520155324-7390c339652a => github.com/lmas/mbaigo v0.0.0-20250715100940-0fef178d190b diff --git a/esr/go.sum b/esr/go.sum new file mode 100644 index 0000000..ef3db4b --- /dev/null +++ b/esr/go.sum @@ -0,0 +1,2 @@ +github.com/lmas/mbaigo v0.0.0-20250715100940-0fef178d190b h1:4I+X0TTj6E2RvaAIG8EV7TzZ9O4oPOOBJiWR/otOOJg= +github.com/lmas/mbaigo v0.0.0-20250715100940-0fef178d190b/go.mod h1:vXE1mDd88Tap9bHm1elrk3Ht8bcImA3FeiSM03yUwsM= From cabbf97c9b481459367f347be520a8e599315910 Mon Sep 17 00:00:00 2001 From: Pake Date: Sun, 20 Jul 2025 23:27:18 +0200 Subject: [PATCH 54/81] Added tests for updateDB() --- esr/esr_test.go | 177 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) diff --git a/esr/esr_test.go b/esr/esr_test.go index 61d2cbb..247c411 100644 --- a/esr/esr_test.go +++ b/esr/esr_test.go @@ -1,11 +1,14 @@ package main import ( + "bytes" "encoding/json" "fmt" "io" + "log" "net/http" "net/http/httptest" + "strings" "testing" "time" @@ -255,3 +258,177 @@ func TestSystemList(t *testing.T) { } } } + +// ----------------------------------------------- // +// Help functions and structs to test updateDB() +// ----------------------------------------------- // + +type updateDBParams struct { + expectedStatuscode int + setup func(*UnitAsset) (*httptest.ResponseRecorder, *http.Request) + testCase string +} + +func TestUpdateDB(t *testing.T) { + params := []updateDBParams{ + { + http.StatusServiceUnavailable, + func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { + ua.leading = false + w = httptest.NewRecorder() + body := io.NopCloser(strings.NewReader("TestBody")) + r = httptest.NewRequest(http.MethodPut, "http://localhost/reg", body) + r.Header = map[string][]string{"Content-Type": {"application/json"}} + + return w, r + }, + "Bad case, not leading registrar", + }, + { + 200, + func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { + ua.leading = true + + w = httptest.NewRecorder() + body := io.NopCloser(strings.NewReader("TestBody")) + r = httptest.NewRequest(http.MethodPut, "http://localhost/reg", body) + + return w, r + }, + "Bad case, wrong content type in request", + }, + { + 200, + func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { + ua.leading = true + + w = httptest.NewRecorder() + body := io.NopCloser(errReader(0)) + r = httptest.NewRequest(http.MethodPut, "http://localhost/reg", body) + r.Header = map[string][]string{"Content-Type": {"application/json"}} + + return w, r + }, + "Bad case, can't read body", + }, + { + 200, + func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { + ua.leading = true + + w = httptest.NewRecorder() + body := io.NopCloser(strings.NewReader("")) + r = httptest.NewRequest(http.MethodPut, "http://localhost/reg", body) + r.Header = map[string][]string{"Content-Type": {"application/json"}} + + return w, r + }, + "Bad case, can't unpack body", + }, + { + http.StatusInternalServerError, + func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { + ua.leading = true + + w = httptest.NewRecorder() + + // Record has to match the one sent by sendAddRequest(..) + rec := &forms.ServiceRecord_v1{ + Id: int(0), + ServiceDefinition: "test", + SystemName: "System", + ServiceNode: "node", + IPAddresses: []string{"123.456.789.012"}, + ProtoPort: map[string]int{"http": 1234}, + Details: map[string][]string{"details": {}}, + Certificate: "ABCD", + SubPath: "testPath", + RegLife: 25, + Version: "SignalA_v1.0", + Created: "", + Updated: time.Now().String(), + EndOfValidity: time.Now().Add(25 * time.Second).String(), + SubscribeAble: false, + ACost: float64(0), + CUnit: "", + } + data, err := json.Marshal(rec) + if err != nil { + log.Printf("Error: %v", err) + } + body := io.NopCloser(bytes.NewReader(data)) + r = httptest.NewRequest(http.MethodPut, "http://localhost/reg", body) + r.Header = map[string][]string{"Content-Type": {"application/json"}} + sendAddRequest(0, "test", "testPath", "", ua.requests) + + return w, r + }, + "Bad case, request returns error", + }, + { + 200, + func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { + ua.leading = true + + w = httptest.NewRecorder() + body := io.NopCloser(strings.NewReader("")) + r = httptest.NewRequest(http.MethodGet, "http://localhost/reg", body) + r.Header = map[string][]string{"Content-Type": {"application/json"}} + + return w, r + }, + "Good case, default case", + }, + } + + for _, c := range params { + // Setup + var ua *UnitAsset + sys := createTestSystem() + confAsset := createConfAssetMultipleTraits() + temp, shutdown := newResource(confAsset, &sys) + ua = temp.(*UnitAsset) + + w, r := c.setup(ua) + + // Test and checks + ua.updateDB(w, r) + + if w.Result().StatusCode != c.expectedStatuscode { + t.Errorf("Expected statuscode %d, got: %d in '%s'", + c.expectedStatuscode, w.Result().StatusCode, c.testCase) + } + + log.Printf("%+v", w.Result()) + + shutdown() + } +} + +// ----------------------------------------------- // +// Help functions and structs to test queryDB() +// ----------------------------------------------- // + +type queryDBParams struct { + expectedStatuscode int + setup func(*UnitAsset) (*httptest.ResponseRecorder, *http.Request) + testCase string +} + +func TestQueryDB(t *testing.T) { + +} + +// ----------------------------------------------- // +// Help functions and structs to test cleanDB() +// ----------------------------------------------- // + +type cleanDBParams struct { + expectedStatuscode int + setup func(*UnitAsset) (*httptest.ResponseRecorder, *http.Request) + testCase string +} + +func TestCleanDB(t *testing.T) { + +} From 22e07acc0d853e9937067fcf2b37ff34ccb32de0 Mon Sep 17 00:00:00 2001 From: walpat-1 Date: Mon, 21 Jul 2025 09:04:37 +0200 Subject: [PATCH 55/81] Added tests for queryDB and cleanDB() --- esr/esr_test.go | 172 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 170 insertions(+), 2 deletions(-) diff --git a/esr/esr_test.go b/esr/esr_test.go index 247c411..1086948 100644 --- a/esr/esr_test.go +++ b/esr/esr_test.go @@ -399,8 +399,6 @@ func TestUpdateDB(t *testing.T) { c.expectedStatuscode, w.Result().StatusCode, c.testCase) } - log.Printf("%+v", w.Result()) - shutdown() } } @@ -416,7 +414,127 @@ type queryDBParams struct { } func TestQueryDB(t *testing.T) { + params := []queryDBParams{ + { + http.StatusOK, + func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { + ua.leading = true + + w = httptest.NewRecorder() + body := io.NopCloser(strings.NewReader("{}")) + r = httptest.NewRequest(http.MethodGet, "http://localhost/reg", body) + r.Header = map[string][]string{"Content-Type": {"application/json"}} + + return w, r + }, + "Good case GET, everything passes", + }, + { + http.StatusOK, + func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { + ua.leading = true + + w = httptest.NewRecorder() + body := io.NopCloser(strings.NewReader("{}")) + r = httptest.NewRequest(http.MethodPost, "http://localhost/reg", body) + r.Header = map[string][]string{} + + return w, r + }, + "Bad case POST, can't parse Content-Type from header", + }, + { + http.StatusOK, + func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { + ua.leading = true + + w = httptest.NewRecorder() + body := io.NopCloser(errReader(0)) + r = httptest.NewRequest(http.MethodPost, "http://localhost/reg", body) + r.Header = map[string][]string{"Content-Type": {"application/json"}} + + return w, r + }, + "Bad case POST, error while reading body", + }, + { + http.StatusOK, + func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { + ua.leading = true + + w = httptest.NewRecorder() + body := io.NopCloser(strings.NewReader("{}")) + r = httptest.NewRequest(http.MethodPost, "http://localhost/reg", body) + r.Header = map[string][]string{"Content-Type": {"application/json"}} + + return w, r + }, + "Bad case POST, error while unpacking body", + }, + { + http.StatusInternalServerError, + func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { + ua.leading = true + + w = httptest.NewRecorder() + body := io.NopCloser(strings.NewReader(`{"id": 0, "version":"SignalA_v1.0"}`)) + r = httptest.NewRequest(http.MethodPost, "http://localhost/reg", body) + r.Header = map[string][]string{"Content-Type": {"application/json"}} + + return w, r + }, + "Bad case POST, request returns error", + }, + { + http.StatusOK, + func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { + ua.leading = true + + w = httptest.NewRecorder() + body := io.NopCloser(strings.NewReader(`{"id": 0, "version":"ServiceQuest_v1"}`)) + r = httptest.NewRequest(http.MethodPost, "http://localhost/reg", body) + r.Header = map[string][]string{"Content-Type": {"application/json"}} + + return w, r + }, + "Good case POST, request returns a result", + }, + { + http.StatusMethodNotAllowed, + func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { + ua.leading = true + w = httptest.NewRecorder() + body := io.NopCloser(strings.NewReader(`{"id": 0, "version":"ServiceQuest_v1"}`)) + r = httptest.NewRequest(http.MethodDelete, "http://localhost/reg", body) + r.Header = map[string][]string{"Content-Type": {"application/json"}} + + return w, r + }, + "Bad case default, unsupported http method", + }, + } + + for _, c := range params { + var ua *UnitAsset + sys := createTestSystem() + confAsset := createConfAssetMultipleTraits() + temp, shutdown := newResource(confAsset, &sys) + ua = temp.(*UnitAsset) + sendAddRequest(0, "test", "testPath", "", ua.requests) + + w, r := c.setup(ua) + + // Test and checks + ua.queryDB(w, r) + + if w.Result().StatusCode != c.expectedStatuscode { + t.Errorf("Expected statuscode %d, got: %d in '%s'", + c.expectedStatuscode, w.Result().StatusCode, c.testCase) + } + + shutdown() + } } // ----------------------------------------------- // @@ -430,5 +548,55 @@ type cleanDBParams struct { } func TestCleanDB(t *testing.T) { + params := []cleanDBParams{ + { + http.StatusBadRequest, + func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { + ua.leading = true + + w = httptest.NewRecorder() + body := io.NopCloser(strings.NewReader(`{"id": 0, "version":"ServiceQuest_v1"}`)) + r = httptest.NewRequest(http.MethodDelete, "http://localhost/reg/a", body) + r.Header = map[string][]string{"Content-Type": {"application/json"}} + + return w, r + }, + "Bad case DELETE, couldn't convert id to int", + }, + { + 200, + func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { + ua.leading = true + w = httptest.NewRecorder() + body := io.NopCloser(strings.NewReader(`{"id": 0, "version":"ServiceQuest_v1"}`)) + r = httptest.NewRequest(http.MethodGet, "http://localhost/reg/a", body) + r.Header = map[string][]string{"Content-Type": {"application/json"}} + + return w, r + }, + "Bad case default, unsupported http method", + }, + } + + for _, c := range params { + var ua *UnitAsset + sys := createTestSystem() + confAsset := createConfAssetMultipleTraits() + temp, shutdown := newResource(confAsset, &sys) + ua = temp.(*UnitAsset) + sendAddRequest(0, "test", "testPath", "", ua.requests) + + w, r := c.setup(ua) + + // Test and checks + ua.cleanDB(w, r) + + if w.Result().StatusCode != c.expectedStatuscode { + t.Errorf("Expected statuscode %d, got: %d in '%s'", + c.expectedStatuscode, w.Result().StatusCode, c.testCase) + } + + shutdown() + } } From ebd706b60719f0c58d030fd96c61cfb98f3a0e74 Mon Sep 17 00:00:00 2001 From: walpat-1 Date: Mon, 21 Jul 2025 09:09:45 +0200 Subject: [PATCH 56/81] Fixed linter errors --- esr/thing_test.go | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/esr/thing_test.go b/esr/thing_test.go index e690164..da6a874 100644 --- a/esr/thing_test.go +++ b/esr/thing_test.go @@ -192,10 +192,12 @@ func sendAddRequest(id int64, def string, subPath string, created string, ch cha } ch <- req - select { - case err := <-req.Error: + + if err := <-req.Error; err != nil { return err } + + return nil } func sendBrokenAddRequest(num int64, ch chan ServiceRegistryRequest) error { @@ -207,10 +209,12 @@ func sendBrokenAddRequest(num int64, ch chan ServiceRegistryRequest) error { Error: make(chan error), } ch <- req - select { - case err := <-req.Error: + + if err := <-req.Error; err != nil { return err } + + return nil } type serviceRegistryHandlerParams struct { @@ -354,10 +358,11 @@ func sendAddRequestWithDetails(id int64, def string, subPath string, created str } ch <- req - select { - case err := <-req.Error: + if err := <-req.Error; err != nil { return err } + + return nil } // id 0 will return all items in service registry, any other will return items depending on details & definition @@ -400,9 +405,8 @@ func sendReadRequest(id int64, def string, details []string, ch chan ServiceRegi func sendBrokenReadRequest(ch chan ServiceRegistryRequest) ([]forms.ServiceRecord_v1, error) { rec := &forms.SignalA_v1a{} - var req ServiceRegistryRequest - req = ServiceRegistryRequest{ + var req = ServiceRegistryRequest{ Action: "read", Record: rec, Result: make(chan []forms.ServiceRecord_v1), From 2ec23d9854e41785adcd5b7694fa9495dee68e0d Mon Sep 17 00:00:00 2001 From: walpat-1 Date: Mon, 21 Jul 2025 09:10:34 +0200 Subject: [PATCH 57/81] Fixed spellchecker errors --- esr/esr.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/esr/esr.go b/esr/esr.go index b5963d0..9219ffc 100644 --- a/esr/esr.go +++ b/esr/esr.go @@ -121,7 +121,7 @@ func (ua *UnitAsset) updateDB(w http.ResponseWriter, r *http.Request) { if !ua.leading { w.WriteHeader(http.StatusServiceUnavailable) if _, err := w.Write([]byte("Service Unavailable")); err != nil { - log.Printf("error occured while writing to responsewriter: %v", err) + log.Printf("error occurreded while writing to responsewriter: %v", err) } return } @@ -206,11 +206,11 @@ func (ua *UnitAsset) queryDB(w http.ResponseWriter, r *http.Request) { // Build the HTML response text := "" if _, err := w.Write([]byte(text)); err != nil { - log.Printf("error occured while writing to responsewriter: %v", err) + log.Printf("error occurreded while writing to responsewriter: %v", err) } text = "

    The local cloud's currently available services are:

      " if _, err := w.Write([]byte(text)); err != nil { - log.Printf("error occured while writing to responsewriter: %v", err) + log.Printf("error occurreded while writing to responsewriter: %v", err) } for _, servRec := range servvicesList { metaservice := "" @@ -222,12 +222,12 @@ func (ua *UnitAsset) queryDB(w http.ResponseWriter, r *http.Request) { uaName := parts[0] sLine := "

      Service ID: " + strconv.Itoa(int(servRec.Id)) + " with definition " + servRec.ServiceDefinition + " from the " + servRec.SystemName + "/" + uaName + " with details " + metaservice + " will expire at: " + servRec.EndOfValidity + "

      " if _, err := w.Write([]byte(fmt.Sprintf("
    • %s
    • ", sLine))); err != nil { - log.Printf("error occured while writing to responsewriter: %v", err) + log.Printf("error occurreded while writing to responsewriter: %v", err) } } text = "
    " if _, err := w.Write([]byte(text)); err != nil { - log.Printf("error occured while writing to responsewriter: %v", err) + log.Printf("error occurreded while writing to responsewriter: %v", err) } case <-time.After(5 * time.Second): // Optional timeout http.Error(w, "Request timed out", http.StatusGatewayTimeout) @@ -350,7 +350,7 @@ func (ua *UnitAsset) roleStatus(w http.ResponseWriter, r *http.Request) { } w.WriteHeader(http.StatusServiceUnavailable) if _, err := w.Write([]byte("Service Unavailable")); err != nil { - log.Printf("error occured while writing to responsewriter: %v", err) + log.Printf("error occurreded while writing to responsewriter: %v", err) } default: fmt.Fprintf(w, "unsupported http request method") From 9101c8e5269860c238024df49d971e78cebeeb40 Mon Sep 17 00:00:00 2001 From: walpat-1 Date: Mon, 21 Jul 2025 17:09:31 +0200 Subject: [PATCH 58/81] Simplified scheduler --- esr/scheduler.go | 127 +++++++---------------------------------------- esr/thing.go | 6 +-- 2 files changed, 22 insertions(+), 111 deletions(-) diff --git a/esr/scheduler.go b/esr/scheduler.go index 91a6340..3765e0d 100644 --- a/esr/scheduler.go +++ b/esr/scheduler.go @@ -16,132 +16,43 @@ package main -import ( - "container/heap" - "time" -) +import "time" -//*********************Expired services cleaning scheduler********************* - -// cleaningTask holds the time for the next time a service is due to expire -type cleaningTask struct { - Deadline time.Time // the time when job has to be executed - Job func() // call to check expiration of a record - Id int // the job Id is the record id and is used to remove a scheduled task -} - -// cleaningQueue the list of schedlued check on service expiration -type cleaningQueue []*cleaningTask - -// Len returns the length of the service list and their expiration time -func (cq cleaningQueue) Len() int { return len(cq) } - -// Less checks if a task is due sooner than another -func (cq cleaningQueue) Less(i, j int) bool { - return cq[i].Deadline.Before(cq[j].Deadline) -} - -// Swap exchanges the order of task if one is due before the other -func (cq cleaningQueue) Swap(i, j int) { - cq[i], cq[j] = cq[j], cq[i] -} - -// Push adds a task to the task list or queue -func (cq *cleaningQueue) Push(x interface{}) { - task := x.(*cleaningTask) - *cq = append(*cq, task) -} - -// Pop remove a task from the cleaning queue -func (cq *cleaningQueue) Pop() interface{} { - old := *cq - n := len(old) - task := old[n-1] - *cq = old[0 : n-1] - return task -} - -// Scheduler struct type with the list and two channels +// Scheduler struct type with the list and three channels type Scheduler struct { - taskQueue cleaningQueue - taskStream chan *cleaningTask - stopChan chan struct{} + taskMap map[int]*time.Timer // list elements has id, timer } // NewScheduler creates a new scheduler func NewScheduler() *Scheduler { return &Scheduler{ - taskStream: make(chan *cleaningTask), - stopChan: make(chan struct{}), + taskMap: make(map[int]*time.Timer), } } // AddTask adds a task to the queue with its deadline func (s *Scheduler) AddTask(deadline time.Time, job func(), id int) { - task := &cleaningTask{ - Deadline: deadline, - Job: job, - Id: id, + timer, exists := s.taskMap[id] + if exists { + timer.Stop() } - s.taskStream <- task + t := time.AfterFunc(time.Until(deadline), job) + s.taskMap[id] = t } // RemoveTask removes a scheduled task -func (s *Scheduler) RemoveTask(id int) bool { - // Search for the task with the given Id - for i, task := range s.taskQueue { - if task.Id == id { - // Remove the task from the queue - s.taskQueue = append(s.taskQueue[:i], s.taskQueue[i+1:]...) - heap.Init(&s.taskQueue) // Reinitialize the heap - return true // Return true indicating the task was found and removed - } +func (s *Scheduler) RemoveTask(id int) { + timer, exists := s.taskMap[id] + if !exists { + return } - return false // Return false if the task wasn't found + timer.Stop() + delete(s.taskMap, id) } -// run is the goroutine that cleans up expired services by continuously checking that end of validity of services -func (s *Scheduler) run() { - var timer *time.Timer - defer s.Stop() - for { - if len(s.taskQueue) > 0 { - nextTask := s.taskQueue[0] - if timer == nil { - timer = time.NewTimer(time.Until(nextTask.Deadline)) - } else { - timer.Reset(time.Until(nextTask.Deadline)) - } - } - - time.Sleep(10 * time.Millisecond) // this is used to reduce CPU consumption otherwise the go routine is a "short circuit" with no resistance - - select { - case task := <-s.taskStream: - heap.Push(&s.taskQueue, task) - if timer == nil { - timer = time.NewTimer(time.Until(task.Deadline)) - } else { - timer.Reset(time.Until(task.Deadline)) - } - case <-func() <-chan time.Time { - if timer != nil { - return timer.C - } - return nil - }(): - task := heap.Pop(&s.taskQueue).(*cleaningTask) - go task.Job() - case <-s.stopChan: - if timer != nil { - timer.Stop() - } - return - } - } -} - -// Stop terminnates the scheduler func (s *Scheduler) Stop() { - s.stopChan <- struct{}{} + for _, value := range s.taskMap { + value.Stop() + } + s.taskMap = make(map[int]*time.Timer) } diff --git a/esr/thing.go b/esr/thing.go index 1ad0591..3b9c2e0 100644 --- a/esr/thing.go +++ b/esr/thing.go @@ -151,7 +151,6 @@ func initTemplate() components.UnitAsset { func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.System) (components.UnitAsset, func()) { // Start the registration expiration check scheduler cleaningScheduler := NewScheduler() - go cleaningScheduler.run() // Initialize the UnitAsset ua := &UnitAsset{ @@ -176,9 +175,10 @@ func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.Sys // Error: make(chan error), // Initialize the error channel } - ua.Traits = assetTraits // Assign the traits to the UnitAsset + ua.Traits = assetTraits - ua.Role() // Start to repeatedly check which is the leading registrar + // Start to repeatedly check which is the leading registrar + ua.Role() // Start the service registry manager goroutine go ua.serviceRegistryHandler() From 3a4ad2196b9df118ce3c2ac3d0b6d0591c74ef9c Mon Sep 17 00:00:00 2001 From: Pake Date: Tue, 22 Jul 2025 12:04:45 +0200 Subject: [PATCH 59/81] Added tests for scheduler --- esr/scheduler_test.go | 65 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 esr/scheduler_test.go diff --git a/esr/scheduler_test.go b/esr/scheduler_test.go new file mode 100644 index 0000000..1da84a9 --- /dev/null +++ b/esr/scheduler_test.go @@ -0,0 +1,65 @@ +package main + +import ( + "testing" + "time" +) + +// --------------------------------------------------------------------------- // +// Help functions and structs to test the add part of **** +// --------------------------------------------------------------------------- // + +type addTaskParams struct { + setup func() *Scheduler +} + +func TestAddTask(t *testing.T) { + sched := NewScheduler() + now := time.Now() + + // Add the task + sched.AddTask(now.Add(25*time.Second), func() {}, 0) + + if _, exists := sched.taskMap[0]; !exists { + t.Errorf("Task was not present") + } +} + +func TestRemoveTask(t *testing.T) { + // Case: task exists + sched := NewScheduler() + now := time.Now() + + // Add the task and then remove it + sched.AddTask(now.Add(25*time.Second), func() {}, 0) + sched.RemoveTask(0) + + if _, exists := sched.taskMap[0]; exists { + t.Errorf("Unexpected task exists in taskMap[0]") + } + + // Case: task doesn't exist + sched = NewScheduler() + // Add the task and then remove it + sched.RemoveTask(0) + + if _, exists := sched.taskMap[0]; exists { + t.Errorf("Unexpected task exists in taskMap[0]") + } +} + +func TestStop(t *testing.T) { + sched := NewScheduler() + now := time.Now() + + // Add the task and then remove it + sched.AddTask(now.Add(25*time.Second), func() {}, 0) + sched.AddTask(now.Add(25*time.Second), func() {}, 1) + sched.AddTask(now.Add(25*time.Second), func() {}, 2) + sched.AddTask(now.Add(25*time.Second), func() {}, 3) + sched.Stop() + + if len(sched.taskMap) > 0 { + t.Errorf("Expected taskMap to be empty, has %d keys", len(sched.taskMap)) + } +} From c461acd61c04501cdb8f7746c5bc8850c2114fd5 Mon Sep 17 00:00:00 2001 From: walpat-1 Date: Tue, 22 Jul 2025 15:06:27 +0200 Subject: [PATCH 60/81] Fixed some errorhandlers, spellings and locks in newResource --- esr/esr.go | 62 +++++++++++++++++++++++++----------------------- esr/scheduler.go | 7 +++--- esr/thing.go | 13 +++++----- 3 files changed, 42 insertions(+), 40 deletions(-) diff --git a/esr/esr.go b/esr/esr.go index 9219ffc..a915a45 100644 --- a/esr/esr.go +++ b/esr/esr.go @@ -92,8 +92,9 @@ func main() { // wait for shutdown signal, and gracefully close properly goroutines with context <-sys.Sigs // wait for a SIGINT (Ctrl+C) signal fmt.Println("\nshuting down system", sys.Name) - cancel() // cancel the context, signaling the goroutines to stop - time.Sleep(3 * time.Second) // allow the go routines to be executed, which might take more time than the main routine to end + cancel() // cancel the context, signaling the goroutines to stop + // allow the go routines to be executed, which might take more time than the main routine to end + time.Sleep(3 * time.Second) } // ---------------------------------------------------------------------------- end of main() @@ -121,7 +122,7 @@ func (ua *UnitAsset) updateDB(w http.ResponseWriter, r *http.Request) { if !ua.leading { w.WriteHeader(http.StatusServiceUnavailable) if _, err := w.Write([]byte("Service Unavailable")); err != nil { - log.Printf("error occurreded while writing to responsewriter: %v", err) + log.Printf("error occurred while writing to responsewriter: %v", err) } return } @@ -130,19 +131,19 @@ func (ua *UnitAsset) updateDB(w http.ResponseWriter, r *http.Request) { contentType := r.Header.Get("Content-Type") mediaType, _, err := mime.ParseMediaType(contentType) if err != nil { - fmt.Println("Error parsing media type:", err) + log.Println("Error parsing media type:", err) return } defer r.Body.Close() bodyBytes, err := io.ReadAll(r.Body) if err != nil { - log.Printf("error reading registration request body: %v", err) + log.Printf("Error reading registration request body: %v", err) return } record, err := usecases.Unpack(bodyBytes, mediaType) if err != nil { - log.Printf("error extracting the registration request %v\n", err) + log.Printf("Error extracting the registration request %v\n", err) return } @@ -158,14 +159,14 @@ func (ua *UnitAsset) updateDB(w http.ResponseWriter, r *http.Request) { // Check the error back from the unit asset err = <-addRecord.Error if err != nil { - log.Printf("error adding the new service: %v", err) + log.Printf("Error adding the new service: %v", err) http.Error(w, "Error registering service", http.StatusInternalServerError) return } // fmt.Println(record) updatedRecordBytes, err := usecases.Pack(record, mediaType) if err != nil { - log.Printf("error confirming new service: %s", err) + log.Printf("Error confirming new service: %s", err) http.Error(w, "Error registering service", http.StatusInternalServerError) } w.Header().Set("Content-Type", mediaType) @@ -202,17 +203,17 @@ func (ua *UnitAsset) queryDB(w http.ResponseWriter, r *http.Request) { log.Printf("Error retrieving service records: %v", err) http.Error(w, "Error retrieving service records", http.StatusInternalServerError) } - case servvicesList := <-recordsRequest.Result: + case servicesList := <-recordsRequest.Result: // Build the HTML response text := "" if _, err := w.Write([]byte(text)); err != nil { - log.Printf("error occurreded while writing to responsewriter: %v", err) + log.Printf("Error occurred while writing to responsewriter: %v", err) } text = "

    The local cloud's currently available services are:

      " if _, err := w.Write([]byte(text)); err != nil { - log.Printf("error occurreded while writing to responsewriter: %v", err) + log.Printf("Error occurred while writing to responsewriter: %v", err) } - for _, servRec := range servvicesList { + for _, servRec := range servicesList { metaservice := "" for key, values := range servRec.Details { metaservice += key + ": " + fmt.Sprintf("%v", values) + " " @@ -222,35 +223,38 @@ func (ua *UnitAsset) queryDB(w http.ResponseWriter, r *http.Request) { uaName := parts[0] sLine := "

      Service ID: " + strconv.Itoa(int(servRec.Id)) + " with definition " + servRec.ServiceDefinition + " from the " + servRec.SystemName + "/" + uaName + " with details " + metaservice + " will expire at: " + servRec.EndOfValidity + "

      " if _, err := w.Write([]byte(fmt.Sprintf("
    • %s
    • ", sLine))); err != nil { - log.Printf("error occurreded while writing to responsewriter: %v", err) + log.Printf("Error occurred while writing to responsewriter: %v", err) } } text = "
    " if _, err := w.Write([]byte(text)); err != nil { - log.Printf("error occurreded while writing to responsewriter: %v", err) + log.Printf("Error occurred while writing to responsewriter: %v", err) } case <-time.After(5 * time.Second): // Optional timeout http.Error(w, "Request timed out", http.StatusGatewayTimeout) log.Println("Failure to process service listing request") } - case "POST": // from the orchesrator + case "POST": // from the orchestrator contentType := r.Header.Get("Content-Type") mediaType, _, err := mime.ParseMediaType(contentType) if err != nil { - fmt.Println("Error parsing media type:", err) + log.Println("Error parsing media type:", err) + http.Error(w, "Error parsing media type", http.StatusBadRequest) return } defer r.Body.Close() bodyBytes, err := io.ReadAll(r.Body) if err != nil { - log.Printf("error reading service discovery request body: %v", err) + log.Printf("Error reading service discovery request body: %v", err) + http.Error(w, "Error reading service discovery request body", http.StatusBadRequest) return } record, err := usecases.Unpack(bodyBytes, mediaType) if err != nil { - log.Printf("error extracting the service discovery request %v\n", err) + log.Printf("Error extracting the service discovery request %v\n", err) + http.Error(w, "Error extracting the service discovery request", http.StatusBadRequest) return } @@ -273,11 +277,10 @@ func (ua *UnitAsset) queryDB(w http.ResponseWriter, r *http.Request) { http.Error(w, "Error retrieving service records", http.StatusInternalServerError) return } - case servvicesList := <-readRecord.Result: - fmt.Println(servvicesList) + case servicesList := <-readRecord.Result: var slForm forms.ServiceRecordList_v1 slForm.NewForm() - slForm.List = servvicesList + slForm.List = servicesList updatedRecordBytes, err := usecases.Pack(&slForm, mediaType) if err != nil { log.Printf("error confirming new service: %s", err) @@ -291,14 +294,13 @@ func (ua *UnitAsset) queryDB(w http.ResponseWriter, r *http.Request) { return } case <-time.After(5 * time.Second): // Optional timeout - http.Error(w, "Request timed out", http.StatusGatewayTimeout) log.Println("Failure to process service discovery request") + http.Error(w, "Request timed out", http.StatusGatewayTimeout) return } default: http.Error(w, "Unsupported HTTP request method", http.StatusMethodNotAllowed) } - // fmt.Println("Done querying the database") } // cleanDB deletes service records upon request (e.g., when a system shuts down) @@ -325,7 +327,7 @@ func (ua *UnitAsset) cleanDB(w http.ResponseWriter, r *http.Request) { // Check the error back from the unit asset err = <-addRecord.Error if err != nil { - log.Printf("error deleting the service with id: %d, %s\n", id, err) + log.Printf("Error deleting the service with id: %d, %s\n", id, err) http.Error(w, "Error deleting service", http.StatusInternalServerError) return } @@ -350,10 +352,10 @@ func (ua *UnitAsset) roleStatus(w http.ResponseWriter, r *http.Request) { } w.WriteHeader(http.StatusServiceUnavailable) if _, err := w.Write([]byte("Service Unavailable")); err != nil { - log.Printf("error occurreded while writing to responsewriter: %v", err) + log.Printf("Error occurred while writing to responsewriter: %v", err) } default: - fmt.Fprintf(w, "unsupported http request method") + fmt.Fprintf(w, "Unsupported http request method") } } @@ -386,14 +388,14 @@ func (ua *UnitAsset) Role() { case http.StatusServiceUnavailable: // Service unavailable default: - fmt.Printf("Received unexpected status code: %d\n", resp.StatusCode) + log.Printf("Received unexpected status code: %d\n", resp.StatusCode) } } if !standby && !ua.leading { ua.leading = true ua.leadingSince = time.Now() ua.leadingRegistrar = nil - fmt.Printf("taking the service registry lead at %s\n", ua.leadingSince) + log.Printf("Taking the service registry lead at %s\n", ua.leadingSince) } <-ticker.C } @@ -428,11 +430,11 @@ func (ua *UnitAsset) systemList(w http.ResponseWriter, r *http.Request) { case "GET": systemsList, err := getUniqueSystems(ua) if err != nil { - http.Error(w, fmt.Sprintf("system list error: %s", err), http.StatusInternalServerError) + http.Error(w, fmt.Sprintf("System list error: %s", err), http.StatusInternalServerError) return } usecases.HTTPProcessGetRequest(w, r, systemsList) default: - http.Error(w, "unsupported HTTP request method", http.StatusMethodNotAllowed) + http.Error(w, "Unsupported HTTP request method", http.StatusMethodNotAllowed) } } diff --git a/esr/scheduler.go b/esr/scheduler.go index 3765e0d..4dd4087 100644 --- a/esr/scheduler.go +++ b/esr/scheduler.go @@ -23,14 +23,14 @@ type Scheduler struct { taskMap map[int]*time.Timer // list elements has id, timer } -// NewScheduler creates a new scheduler +// Returns a scheduler with an empty task map func NewScheduler() *Scheduler { return &Scheduler{ taskMap: make(map[int]*time.Timer), } } -// AddTask adds a task to the queue with its deadline +// AddTask adds a task to the task map and starts a timer for its job, when timer is done it runs the job in a goroutine func (s *Scheduler) AddTask(deadline time.Time, job func(), id int) { timer, exists := s.taskMap[id] if exists { @@ -40,7 +40,7 @@ func (s *Scheduler) AddTask(deadline time.Time, job func(), id int) { s.taskMap[id] = t } -// RemoveTask removes a scheduled task +// RemoveTask removes a scheduled job and deletes the task from the task map func (s *Scheduler) RemoveTask(id int) { timer, exists := s.taskMap[id] if !exists { @@ -50,6 +50,7 @@ func (s *Scheduler) RemoveTask(id int) { delete(s.taskMap, id) } +// Stop() loops through the task map and turns off the timer for each tasks job func (s *Scheduler) Stop() { for _, value := range s.taskMap { value.Stop() diff --git a/esr/thing.go b/esr/thing.go index 3b9c2e0..566a735 100644 --- a/esr/thing.go +++ b/esr/thing.go @@ -175,17 +175,19 @@ func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.Sys // Error: make(chan error), // Initialize the error channel } - ua.Traits = assetTraits + ua.Traits = assetTraits // Start to repeatedly check which is the leading registrar - ua.Role() + ua.Role() // Start the service registry manager goroutine go ua.serviceRegistryHandler() return ua, func() { + ua.mu.Lock() close(ua.requests) // Close channels before exiting (cleanup) cleaningScheduler.Stop() // Gracefully stop the scheduler + ua.mu.Unlock() log.Println("Closing the service registry database connection") } } @@ -279,7 +281,6 @@ func (ua *UnitAsset) serviceRegistryHandler() { } nextExpiration := now.Add(time.Duration(dbRec.RegLife) * time.Second).Format(time.RFC3339) rec.EndOfValidity = nextExpiration - // log.Printf("Updated the record %s with next expiration date at %s", rec.ServiceDefinition, rec.EndOfValidity) } ua.sched.AddTask(now.Add(time.Duration(rec.RegLife)*time.Second), func() { checkExpiration(ua, rec.Id) }, rec.Id) ua.serviceRegistry[rec.Id] = *rec // Add record to the registry @@ -297,16 +298,14 @@ func (ua *UnitAsset) serviceRegistryHandler() { } ua.mu.Unlock() // Unlock access to the service registry map request.Result <- result - // log.Println("complete listing sent from registry") continue } qform, ok := request.Record.(*forms.ServiceQuest_v1) if !ok { - fmt.Println("Problem unpacking the service quest request") + log.Println("Problem unpacking the service quest request") request.Error <- fmt.Errorf("invalid record type") continue } - fmt.Printf("\nThe service quest form is %v\n\n", qform) matchingRecords := ua.FilterByServiceDefinitionAndDetails(qform.ServiceDefinition, qform.Details) request.Result <- matchingRecords @@ -375,7 +374,7 @@ func checkExpiration(ua *UnitAsset, servId int) { dbRec := ua.serviceRegistry[servId] expiration, err := time.Parse(time.RFC3339, dbRec.EndOfValidity) if err != nil { - log.Printf("time parsing problem when checking service expiration") + log.Printf("Time parsing problem when checking service expiration") return } From ec116f8ffc1ddd1abfe78ed90310d0e3125daaa5 Mon Sep 17 00:00:00 2001 From: walpat-1 Date: Tue, 22 Jul 2025 15:25:48 +0200 Subject: [PATCH 61/81] fixed test after changes in esr.go --- esr/esr_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esr/esr_test.go b/esr/esr_test.go index 1086948..b1bc221 100644 --- a/esr/esr_test.go +++ b/esr/esr_test.go @@ -430,7 +430,7 @@ func TestQueryDB(t *testing.T) { "Good case GET, everything passes", }, { - http.StatusOK, + http.StatusBadRequest, func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { ua.leading = true @@ -444,7 +444,7 @@ func TestQueryDB(t *testing.T) { "Bad case POST, can't parse Content-Type from header", }, { - http.StatusOK, + http.StatusBadRequest, func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { ua.leading = true @@ -458,7 +458,7 @@ func TestQueryDB(t *testing.T) { "Bad case POST, error while reading body", }, { - http.StatusOK, + http.StatusBadRequest, func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { ua.leading = true From fb87c6abe43713b31e93d84d5de4cbd33d26e278 Mon Sep 17 00:00:00 2001 From: walpat-1 Date: Tue, 22 Jul 2025 15:26:36 +0200 Subject: [PATCH 62/81] Fixed comments and removed unused struct --- esr/scheduler_test.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/esr/scheduler_test.go b/esr/scheduler_test.go index 1da84a9..d0b2122 100644 --- a/esr/scheduler_test.go +++ b/esr/scheduler_test.go @@ -5,13 +5,9 @@ import ( "time" ) -// --------------------------------------------------------------------------- // -// Help functions and structs to test the add part of **** -// --------------------------------------------------------------------------- // - -type addTaskParams struct { - setup func() *Scheduler -} +// -------------------- // +// Tests for scheduler +// -------------------- // func TestAddTask(t *testing.T) { sched := NewScheduler() @@ -52,7 +48,7 @@ func TestStop(t *testing.T) { sched := NewScheduler() now := time.Now() - // Add the task and then remove it + // Add some tasks and make sure Stop() works as intended sched.AddTask(now.Add(25*time.Second), func() {}, 0) sched.AddTask(now.Add(25*time.Second), func() {}, 1) sched.AddTask(now.Add(25*time.Second), func() {}, 2) From 42a07f31c04eccf8353f66cf7b6af2a82a410d92 Mon Sep 17 00:00:00 2001 From: walpat-1 Date: Tue, 22 Jul 2025 15:27:28 +0200 Subject: [PATCH 63/81] Added delete test for serviceRegistryHandler() --- esr/thing_test.go | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/esr/thing_test.go b/esr/thing_test.go index da6a874..4c6f959 100644 --- a/esr/thing_test.go +++ b/esr/thing_test.go @@ -328,7 +328,7 @@ func TestServiceRegistryHandlerAdd(t *testing.T) { func sendAddRequestWithDetails(id int64, def string, subPath string, created string, ch chan ServiceRegistryRequest) error { rec := &forms.ServiceRecord_v1{ - Id: 0, + Id: int(id), ServiceDefinition: def, SystemName: "System", ServiceNode: "node", @@ -353,6 +353,7 @@ func sendAddRequestWithDetails(id int64, def string, subPath string, created str req := ServiceRegistryRequest{ Action: "add", + Id: 0, Record: rec, Error: make(chan error), } @@ -483,6 +484,32 @@ func TestServiceRegistryHandlerRead(t *testing.T) { } } +// ------------------------------------------------------------------------ // +// Help functions and structs to test delete in serviceRegistryHandler() +// ------------------------------------------------------------------------ // + +func sendDeleteRequest(id int, ch chan ServiceRegistryRequest) { + ch <- ServiceRegistryRequest{ + Action: "delete", + Id: int64(id), + } +} + +func TestServiceRegistryHandlerDelete(t *testing.T) { + // Setup + temp := createConfAssetMultipleTraits() + sys := createNewSys() + res, shutdown := newResource(temp, &sys) + ua, _ := res.(*UnitAsset) + time.Sleep(25 * time.Millisecond) + // Add a services to the serviceregistrar + sendAddRequestWithDetails(1, "test", "sub1", time.Now().Format(time.RFC3339), ua.requests) + + sendDeleteRequest(0, ua.requests) + + shutdown() +} + // ------------------------------------------------------------------------ // // Help functions and structs to test FilterByServiceDefinitionAndDetails() // ------------------------------------------------------------------------ // From 1791ee6f05bc681428147e11d96240224db1469e Mon Sep 17 00:00:00 2001 From: walpat-1 Date: Tue, 22 Jul 2025 16:28:48 +0200 Subject: [PATCH 64/81] Added missing http.Error() in error handlers for updateDB() --- esr/esr.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esr/esr.go b/esr/esr.go index a915a45..4a93b40 100644 --- a/esr/esr.go +++ b/esr/esr.go @@ -91,7 +91,7 @@ func main() { // wait for shutdown signal, and gracefully close properly goroutines with context <-sys.Sigs // wait for a SIGINT (Ctrl+C) signal - fmt.Println("\nshuting down system", sys.Name) + fmt.Println("\nShutting down system", sys.Name) cancel() // cancel the context, signaling the goroutines to stop // allow the go routines to be executed, which might take more time than the main routine to end time.Sleep(3 * time.Second) @@ -132,6 +132,7 @@ func (ua *UnitAsset) updateDB(w http.ResponseWriter, r *http.Request) { mediaType, _, err := mime.ParseMediaType(contentType) if err != nil { log.Println("Error parsing media type:", err) + http.Error(w, "Error parsing media type", http.StatusBadRequest) return } @@ -139,11 +140,13 @@ func (ua *UnitAsset) updateDB(w http.ResponseWriter, r *http.Request) { bodyBytes, err := io.ReadAll(r.Body) if err != nil { log.Printf("Error reading registration request body: %v", err) + http.Error(w, "Error reading registration request body", http.StatusBadRequest) return } record, err := usecases.Unpack(bodyBytes, mediaType) if err != nil { log.Printf("Error extracting the registration request %v\n", err) + http.Error(w, "Error extracting the registration request", http.StatusBadRequest) return } From 351285e36f605839b81bc95506bc0f132ede7800 Mon Sep 17 00:00:00 2001 From: walpat-1 Date: Tue, 22 Jul 2025 17:34:57 +0200 Subject: [PATCH 65/81] fixed errorhandlers in esr.go, and added testcase in esr_test.go --- esr/esr.go | 2 +- esr/esr_test.go | 32 ++++++++++++++++++++++---------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/esr/esr.go b/esr/esr.go index 4a93b40..75c86bd 100644 --- a/esr/esr.go +++ b/esr/esr.go @@ -176,7 +176,7 @@ func (ua *UnitAsset) updateDB(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) _, err = w.Write([]byte(updatedRecordBytes)) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + log.Printf("Error occurred while writing to response: %v", err) return } diff --git a/esr/esr_test.go b/esr/esr_test.go index b1bc221..f60e1b7 100644 --- a/esr/esr_test.go +++ b/esr/esr_test.go @@ -279,48 +279,41 @@ func TestUpdateDB(t *testing.T) { body := io.NopCloser(strings.NewReader("TestBody")) r = httptest.NewRequest(http.MethodPut, "http://localhost/reg", body) r.Header = map[string][]string{"Content-Type": {"application/json"}} - return w, r }, "Bad case, not leading registrar", }, { - 200, + http.StatusBadRequest, func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { ua.leading = true - w = httptest.NewRecorder() body := io.NopCloser(strings.NewReader("TestBody")) r = httptest.NewRequest(http.MethodPut, "http://localhost/reg", body) - return w, r }, "Bad case, wrong content type in request", }, { - 200, + http.StatusBadRequest, func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { ua.leading = true - w = httptest.NewRecorder() body := io.NopCloser(errReader(0)) r = httptest.NewRequest(http.MethodPut, "http://localhost/reg", body) r.Header = map[string][]string{"Content-Type": {"application/json"}} - return w, r }, "Bad case, can't read body", }, { - 200, + http.StatusBadRequest, func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { ua.leading = true - w = httptest.NewRecorder() body := io.NopCloser(strings.NewReader("")) r = httptest.NewRequest(http.MethodPut, "http://localhost/reg", body) r.Header = map[string][]string{"Content-Type": {"application/json"}} - return w, r }, "Bad case, can't unpack body", @@ -370,6 +363,25 @@ func TestUpdateDB(t *testing.T) { func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { ua.leading = true + rec := &forms.ServiceRecord_v1{ + Id: 0, + Version: "ServiceRecord_v1", + } + + data, _ := json.Marshal(rec) + w = httptest.NewRecorder() + body := io.NopCloser(bytes.NewReader(data)) + r = httptest.NewRequest(http.MethodPost, "http://localhost/reg", body) + r.Header = map[string][]string{"Content-Type": {"application/json"}} + + return w, r + }, + "Good case, everything passes", + }, + { + 200, + func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { + ua.leading = true w = httptest.NewRecorder() body := io.NopCloser(strings.NewReader("")) r = httptest.NewRequest(http.MethodGet, "http://localhost/reg", body) From 61a2d97a70b36f278e13e5e0d34cede9575a552a Mon Sep 17 00:00:00 2001 From: Pake Date: Wed, 23 Jul 2025 12:41:38 +0200 Subject: [PATCH 66/81] Added functioncall in checkExpiration() to remove task from scheduler --- esr/thing.go | 1 + 1 file changed, 1 insertion(+) diff --git a/esr/thing.go b/esr/thing.go index 566a735..8852e61 100644 --- a/esr/thing.go +++ b/esr/thing.go @@ -383,6 +383,7 @@ func checkExpiration(ua *UnitAsset, servId int) { return } delete(ua.serviceRegistry, int(servId)) + ua.sched.RemoveTask(int(servId)) if _, exists := ua.serviceRegistry[servId]; !exists { log.Printf("The service with ID %d has been deleted because it was not renewed.", servId) } From 8ef2d6b1589e9f9c425ffc2554f4b8dabb31d1b4 Mon Sep 17 00:00:00 2001 From: Pake Date: Wed, 23 Jul 2025 12:42:19 +0200 Subject: [PATCH 67/81] Fixed PR comments --- esr/esr_test.go | 320 ++++++++++++++++++---------------------------- esr/thing_test.go | 133 +++++++------------ 2 files changed, 172 insertions(+), 281 deletions(-) diff --git a/esr/esr_test.go b/esr/esr_test.go index f60e1b7..9a12071 100644 --- a/esr/esr_test.go +++ b/esr/esr_test.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "io" - "log" "net/http" "net/http/httptest" "strings" @@ -193,8 +192,12 @@ func createFilledRegistrar() *UnitAsset { var serviceAmount int for x := range 5 { serviceAmount++ - ua.serviceRegistry[x] = forms.ServiceRecord_v1{Id: x, SystemName: fmt.Sprintf("testSys%d", x), - IPAddresses: []string{"localhost"}, ProtoPort: map[string]int{"http": 1234}} + ua.serviceRegistry[x] = forms.ServiceRecord_v1{ + Id: x, + SystemName: fmt.Sprintf("testSys%d", x), + IPAddresses: []string{"localhost"}, + ProtoPort: map[string]int{"http": 1234}, + } } return ua } @@ -263,9 +266,47 @@ func TestSystemList(t *testing.T) { // Help functions and structs to test updateDB() // ----------------------------------------------- // +func createSpecialRequest(statusCode int, method string) *http.Request { + if statusCode == 200 { + rec := &forms.ServiceRecord_v1{ + Id: 0, + Version: "ServiceRecord_v1", + } + + data, _ := json.Marshal(rec) + body := io.NopCloser(bytes.NewReader(data)) + return httptest.NewRequest(method, "http://localhost/reg", body) + } else { + rec := &forms.ServiceRecord_v1{ + Id: int(0), + ServiceDefinition: "test", + SystemName: "System", + ServiceNode: "node", + IPAddresses: []string{"123.456.789.012"}, + ProtoPort: map[string]int{"http": 1234}, + Details: map[string][]string{"details": {}}, + Certificate: "ABCD", + SubPath: "testPath", + RegLife: 25, + Version: "SignalA_v1.0", + Created: "", + Updated: time.Now().String(), + EndOfValidity: time.Now().Add(25 * time.Second).String(), + SubscribeAble: false, + ACost: float64(0), + CUnit: "", + } + data, _ := json.Marshal(rec) + body := io.NopCloser(bytes.NewReader(data)) + return httptest.NewRequest(method, "http://localhost/reg", body) + } +} + type updateDBParams struct { expectedStatuscode int - setup func(*UnitAsset) (*httptest.ResponseRecorder, *http.Request) + leading bool + body io.ReadCloser + method string testCase string } @@ -273,122 +314,51 @@ func TestUpdateDB(t *testing.T) { params := []updateDBParams{ { http.StatusServiceUnavailable, - func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { - ua.leading = false - w = httptest.NewRecorder() - body := io.NopCloser(strings.NewReader("TestBody")) - r = httptest.NewRequest(http.MethodPut, "http://localhost/reg", body) - r.Header = map[string][]string{"Content-Type": {"application/json"}} - return w, r - }, + false, + io.NopCloser(strings.NewReader("TestBody")), + http.MethodPut, "Bad case, not leading registrar", }, { http.StatusBadRequest, - func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { - ua.leading = true - w = httptest.NewRecorder() - body := io.NopCloser(strings.NewReader("TestBody")) - r = httptest.NewRequest(http.MethodPut, "http://localhost/reg", body) - return w, r - }, + true, + io.NopCloser(strings.NewReader("TestBody")), + http.MethodPut, "Bad case, wrong content type in request", }, { http.StatusBadRequest, - func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { - ua.leading = true - w = httptest.NewRecorder() - body := io.NopCloser(errReader(0)) - r = httptest.NewRequest(http.MethodPut, "http://localhost/reg", body) - r.Header = map[string][]string{"Content-Type": {"application/json"}} - return w, r - }, + true, + io.NopCloser(errReader(0)), + http.MethodPut, "Bad case, can't read body", }, { http.StatusBadRequest, - func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { - ua.leading = true - w = httptest.NewRecorder() - body := io.NopCloser(strings.NewReader("")) - r = httptest.NewRequest(http.MethodPut, "http://localhost/reg", body) - r.Header = map[string][]string{"Content-Type": {"application/json"}} - return w, r - }, + true, + io.NopCloser(strings.NewReader("")), + http.MethodPut, "Bad case, can't unpack body", }, { http.StatusInternalServerError, - func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { - ua.leading = true - - w = httptest.NewRecorder() - - // Record has to match the one sent by sendAddRequest(..) - rec := &forms.ServiceRecord_v1{ - Id: int(0), - ServiceDefinition: "test", - SystemName: "System", - ServiceNode: "node", - IPAddresses: []string{"123.456.789.012"}, - ProtoPort: map[string]int{"http": 1234}, - Details: map[string][]string{"details": {}}, - Certificate: "ABCD", - SubPath: "testPath", - RegLife: 25, - Version: "SignalA_v1.0", - Created: "", - Updated: time.Now().String(), - EndOfValidity: time.Now().Add(25 * time.Second).String(), - SubscribeAble: false, - ACost: float64(0), - CUnit: "", - } - data, err := json.Marshal(rec) - if err != nil { - log.Printf("Error: %v", err) - } - body := io.NopCloser(bytes.NewReader(data)) - r = httptest.NewRequest(http.MethodPut, "http://localhost/reg", body) - r.Header = map[string][]string{"Content-Type": {"application/json"}} - sendAddRequest(0, "test", "testPath", "", ua.requests) - - return w, r - }, + true, + nil, + http.MethodPut, "Bad case, request returns error", }, { 200, - func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { - ua.leading = true - - rec := &forms.ServiceRecord_v1{ - Id: 0, - Version: "ServiceRecord_v1", - } - - data, _ := json.Marshal(rec) - w = httptest.NewRecorder() - body := io.NopCloser(bytes.NewReader(data)) - r = httptest.NewRequest(http.MethodPost, "http://localhost/reg", body) - r.Header = map[string][]string{"Content-Type": {"application/json"}} - - return w, r - }, + true, + nil, + http.MethodPost, "Good case, everything passes", }, { 200, - func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { - ua.leading = true - w = httptest.NewRecorder() - body := io.NopCloser(strings.NewReader("")) - r = httptest.NewRequest(http.MethodGet, "http://localhost/reg", body) - r.Header = map[string][]string{"Content-Type": {"application/json"}} - - return w, r - }, + true, + io.NopCloser(strings.NewReader("")), + http.MethodGet, "Good case, default case", }, } @@ -400,8 +370,16 @@ func TestUpdateDB(t *testing.T) { confAsset := createConfAssetMultipleTraits() temp, shutdown := newResource(confAsset, &sys) ua = temp.(*UnitAsset) + ua.leading = c.leading + w := httptest.NewRecorder() + var r *http.Request + if c.body == nil { + r = createSpecialRequest(c.expectedStatuscode, c.method) + } else { + r = httptest.NewRequest(c.method, "http://localhost/reg", c.body) + } - w, r := c.setup(ua) + r.Header = map[string][]string{"Content-Type": {"application/json"}} // Test and checks ua.updateDB(w, r) @@ -421,7 +399,10 @@ func TestUpdateDB(t *testing.T) { type queryDBParams struct { expectedStatuscode int - setup func(*UnitAsset) (*httptest.ResponseRecorder, *http.Request) + leading bool + body io.ReadCloser + method string + header map[string][]string testCase string } @@ -429,113 +410,75 @@ func TestQueryDB(t *testing.T) { params := []queryDBParams{ { http.StatusOK, - func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { - ua.leading = true - - w = httptest.NewRecorder() - body := io.NopCloser(strings.NewReader("{}")) - r = httptest.NewRequest(http.MethodGet, "http://localhost/reg", body) - r.Header = map[string][]string{"Content-Type": {"application/json"}} - - return w, r - }, + true, + io.NopCloser(strings.NewReader("{}")), + http.MethodGet, + map[string][]string{"Content-Type": {"application/json"}}, "Good case GET, everything passes", }, { http.StatusBadRequest, - func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { - ua.leading = true - - w = httptest.NewRecorder() - body := io.NopCloser(strings.NewReader("{}")) - r = httptest.NewRequest(http.MethodPost, "http://localhost/reg", body) - r.Header = map[string][]string{} - - return w, r - }, + true, + io.NopCloser(strings.NewReader("{}")), + http.MethodPost, + map[string][]string{}, "Bad case POST, can't parse Content-Type from header", }, { http.StatusBadRequest, - func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { - ua.leading = true - - w = httptest.NewRecorder() - body := io.NopCloser(errReader(0)) - r = httptest.NewRequest(http.MethodPost, "http://localhost/reg", body) - r.Header = map[string][]string{"Content-Type": {"application/json"}} - - return w, r - }, + true, + io.NopCloser(errReader(0)), + http.MethodPost, + map[string][]string{"Content-Type": {"application/json"}}, "Bad case POST, error while reading body", }, { http.StatusBadRequest, - func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { - ua.leading = true - - w = httptest.NewRecorder() - body := io.NopCloser(strings.NewReader("{}")) - r = httptest.NewRequest(http.MethodPost, "http://localhost/reg", body) - r.Header = map[string][]string{"Content-Type": {"application/json"}} - - return w, r - }, + true, + io.NopCloser(strings.NewReader("{}")), + http.MethodPost, + map[string][]string{"Content-Type": {"application/json"}}, "Bad case POST, error while unpacking body", }, { http.StatusInternalServerError, - func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { - ua.leading = true - - w = httptest.NewRecorder() - body := io.NopCloser(strings.NewReader(`{"id": 0, "version":"SignalA_v1.0"}`)) - r = httptest.NewRequest(http.MethodPost, "http://localhost/reg", body) - r.Header = map[string][]string{"Content-Type": {"application/json"}} - - return w, r - }, + true, + io.NopCloser(strings.NewReader(`{"id": 0, "version":"SignalA_v1.0"}`)), + http.MethodPost, + map[string][]string{"Content-Type": {"application/json"}}, "Bad case POST, request returns error", }, { http.StatusOK, - func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { - ua.leading = true - - w = httptest.NewRecorder() - body := io.NopCloser(strings.NewReader(`{"id": 0, "version":"ServiceQuest_v1"}`)) - r = httptest.NewRequest(http.MethodPost, "http://localhost/reg", body) - r.Header = map[string][]string{"Content-Type": {"application/json"}} - - return w, r - }, + true, + io.NopCloser(strings.NewReader(`{"id": 0, "version":"ServiceQuest_v1"}`)), + http.MethodPost, + map[string][]string{"Content-Type": {"application/json"}}, "Good case POST, request returns a result", }, { http.StatusMethodNotAllowed, - func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { - ua.leading = true - - w = httptest.NewRecorder() - body := io.NopCloser(strings.NewReader(`{"id": 0, "version":"ServiceQuest_v1"}`)) - r = httptest.NewRequest(http.MethodDelete, "http://localhost/reg", body) - r.Header = map[string][]string{"Content-Type": {"application/json"}} - - return w, r - }, + true, + io.NopCloser(strings.NewReader(`{"id": 0, "version":"ServiceQuest_v1"}`)), + http.MethodDelete, + map[string][]string{"Content-Type": {"application/json"}}, "Bad case default, unsupported http method", }, } for _, c := range params { + // Setup var ua *UnitAsset sys := createTestSystem() confAsset := createConfAssetMultipleTraits() temp, shutdown := newResource(confAsset, &sys) ua = temp.(*UnitAsset) - sendAddRequest(0, "test", "testPath", "", ua.requests) + ua.leading = c.leading + w := httptest.NewRecorder() + r := httptest.NewRequest(c.method, "http://localhost/reg", c.body) + r.Header = c.header - w, r := c.setup(ua) + sendAddRequest(0, "test", "testPath", "", ua.requests) // Test and checks ua.queryDB(w, r) @@ -555,7 +498,9 @@ func TestQueryDB(t *testing.T) { type cleanDBParams struct { expectedStatuscode int - setup func(*UnitAsset) (*httptest.ResponseRecorder, *http.Request) + leading bool + body io.ReadCloser + method string testCase string } @@ -563,30 +508,16 @@ func TestCleanDB(t *testing.T) { params := []cleanDBParams{ { http.StatusBadRequest, - func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { - ua.leading = true - - w = httptest.NewRecorder() - body := io.NopCloser(strings.NewReader(`{"id": 0, "version":"ServiceQuest_v1"}`)) - r = httptest.NewRequest(http.MethodDelete, "http://localhost/reg/a", body) - r.Header = map[string][]string{"Content-Type": {"application/json"}} - - return w, r - }, + true, + io.NopCloser(strings.NewReader(`{"id": 0, "version":"ServiceQuest_v1"}`)), + http.MethodDelete, "Bad case DELETE, couldn't convert id to int", }, { 200, - func(ua *UnitAsset) (w *httptest.ResponseRecorder, r *http.Request) { - ua.leading = true - - w = httptest.NewRecorder() - body := io.NopCloser(strings.NewReader(`{"id": 0, "version":"ServiceQuest_v1"}`)) - r = httptest.NewRequest(http.MethodGet, "http://localhost/reg/a", body) - r.Header = map[string][]string{"Content-Type": {"application/json"}} - - return w, r - }, + true, + io.NopCloser(strings.NewReader(`{"id": 0, "version":"ServiceQuest_v1"}`)), + http.MethodGet, "Bad case default, unsupported http method", }, } @@ -597,9 +528,12 @@ func TestCleanDB(t *testing.T) { confAsset := createConfAssetMultipleTraits() temp, shutdown := newResource(confAsset, &sys) ua = temp.(*UnitAsset) - sendAddRequest(0, "test", "testPath", "", ua.requests) + ua.leading = c.leading - w, r := c.setup(ua) + w := httptest.NewRecorder() + r := httptest.NewRequest(c.method, "http://localhost/reg/a", c.body) + r.Header = map[string][]string{"Content-Type": {"application/json"}} + sendAddRequest(0, "test", "testPath", "", ua.requests) // Test and checks ua.cleanDB(w, r) diff --git a/esr/thing_test.go b/esr/thing_test.go index 4c6f959..ecfc36e 100644 --- a/esr/thing_test.go +++ b/esr/thing_test.go @@ -13,30 +13,8 @@ import ( "github.com/sdoque/mbaigo/usecases" ) -// ----------------------------------------------------- // -// Help functions and structs to tests initTemplate() -// ----------------------------------------------------- // - -func TestInitTemplate(t *testing.T) { - expectedServices := []string{"register", "query", "unregister", "status"} - - ua := initTemplate() - services := ua.GetServices() - - // Check if expected name and services are present - if ua.GetName() != "registry" { - t.Errorf("Name mismatch expected 'registry', got: %s", ua.GetName()) - } - - for _, s := range expectedServices { - if _, ok := services[s]; !ok { - t.Errorf("Expected service '%s' to be present", s) - } - } -} - // ------------------------------------------------ // -// Help functions and structs to test newResource() +// Help functions and other goodies for testing // ------------------------------------------------ // // Create a error reader to break json.Unmarshal() @@ -51,17 +29,6 @@ func (errReader) Close() error { return nil } -func createConfAssetBrokenTraits() usecases.ConfigurableAsset { - brokenTrait, _ := json.Marshal(errReader(0)) - uac := usecases.ConfigurableAsset{ - Name: "testRegistrar", - Details: map[string][]string{"testDetail": {"detail1", "detail2"}}, - Services: []components.Service{}, - Traits: []json.RawMessage{json.RawMessage(brokenTrait)}, - } - return uac -} - func createConfAssetMultipleTraits() usecases.ConfigurableAsset { uac := usecases.ConfigurableAsset{ Name: "testRegistrar", @@ -97,38 +64,6 @@ func createTestSystem() components.System { return sys } -type newResourceParams struct { - setup func() components.System - confAsset func() usecases.ConfigurableAsset - testCase string -} - -func TestNewResource(t *testing.T) { - params := []newResourceParams{ - { - func() (sys components.System) { return createTestSystem() }, - func() (confAsset usecases.ConfigurableAsset) { return createConfAssetBrokenTraits() }, - "Case: unmarshal traits fails", - }, - { - func() (sys components.System) { return createTestSystem() }, - func() (confAsset usecases.ConfigurableAsset) { return createConfAssetMultipleTraits() }, - "Case: confAsset has multiple traits", - }, - } - - for _, c := range params { - sys := c.setup() - uac := c.confAsset() - - ua, shutdown := newResource(uac, &sys) - shutdown() - if ua.GetName() != "testRegistrar" { - t.Errorf("Name mismatch, expected '%s' got '%s'", uac.Name, ua.GetName()) - } - } -} - // --------------------------------------------------------------------------- // // Help functions and structs to test the add part of serviceRegistryHandler() // --------------------------------------------------------------------------- // @@ -247,8 +182,11 @@ func TestServiceRegistryHandlerAdd(t *testing.T) { { true, func(ua *UnitAsset) error { - sendAddRequest(0, "testDef", "subP", time.Now().Format(time.RFC3339), ua.requests) - err := sendAddRequest(1, "testDef2", "subP", time.Now().Format(time.RFC3339), ua.requests) + err := sendAddRequest(0, "testDef", "subP", time.Now().Format(time.RFC3339), ua.requests) + if err != nil { + return err + } + err = sendAddRequest(1, "testDef2", "subP", time.Now().Format(time.RFC3339), ua.requests) return err }, "Bad case, exists with different service definition", @@ -256,8 +194,11 @@ func TestServiceRegistryHandlerAdd(t *testing.T) { { true, func(ua *UnitAsset) error { - sendAddRequest(0, "testDef", "subP", time.Now().Format(time.RFC3339), ua.requests) - err := sendAddRequest(1, "testDef", "subPa", time.Now().Format(time.RFC3339), ua.requests) + err := sendAddRequest(0, "testDef", "subP", time.Now().Format(time.RFC3339), ua.requests) + if err != nil { + return err + } + err = sendAddRequest(1, "testDef", "subPa", time.Now().Format(time.RFC3339), ua.requests) return err }, "Bad case, exists with different subpath", @@ -265,8 +206,11 @@ func TestServiceRegistryHandlerAdd(t *testing.T) { { true, func(ua *UnitAsset) error { - sendAddRequest(0, "testDef", "subP", time.Now().Format(time.RFC3339), ua.requests) - err := sendAddRequest(1, "testDef", "subP", "", ua.requests) + err := sendAddRequest(0, "testDef", "subP", time.Now().Format(time.RFC3339), ua.requests) + if err != nil { + return err + } + err = sendAddRequest(1, "testDef", "subP", "", ua.requests) return err }, "Bad case, exists different creation time in updated record", @@ -275,8 +219,11 @@ func TestServiceRegistryHandlerAdd(t *testing.T) { true, func(ua *UnitAsset) error { ch := ua.requests - sendAddRequest(0, "testDef", "subP", time.Now().Format(time.RFC3339), ch) - err := sendAddRequest(1, "testDef", "subP", time.Now().Add(1*time.Hour).Format(time.RFC3339), ch) + err := sendAddRequest(0, "testDef", "subP", time.Now().Format(time.RFC3339), ch) + if err != nil { + return err + } + err = sendAddRequest(1, "testDef", "subP", time.Now().Add(1*time.Hour).Format(time.RFC3339), ch) return err }, "Bad case, mismatch between db- and received created field", @@ -285,18 +232,24 @@ func TestServiceRegistryHandlerAdd(t *testing.T) { false, func(ua *UnitAsset) error { ch := ua.requests - sendAddRequest(0, "testDef", "subP", time.Now().Format(time.RFC3339), ch) err := sendAddRequest(0, "testDef", "subP", time.Now().Format(time.RFC3339), ch) + if err != nil { + return err + } + err = sendAddRequest(0, "testDef", "subP", time.Now().Format(time.RFC3339), ch) return err }, - "Bad case, recCount has looped back to 0", + "Good case, recCount has looped back to 0", }, { false, func(ua *UnitAsset) error { ch := ua.requests - sendAddRequest(0, "testDef", "subP", time.Now().Format(time.RFC3339), ch) - err := sendAddRequest(1, "testDef", "subP", time.Now().Format(time.RFC3339), ch) + err := sendAddRequest(0, "testDef", "subP", time.Now().Format(time.RFC3339), ch) + if err != nil { + return err + } + err = sendAddRequest(1, "testDef", "subP", time.Now().Format(time.RFC3339), ch) return err }, "Good case, updated db record", @@ -312,6 +265,7 @@ func TestServiceRegistryHandlerAdd(t *testing.T) { // Test and check err := c.request(ua) + if c.expectError == false && err != nil { t.Errorf("Expected no errors in '%s': %v", c.testCase, err) } @@ -581,11 +535,12 @@ func TestFilterByServiceDefAndDetails(t *testing.T) { // Help functions and structs to test checkExpiration() // ---------------------------------------------------- // -func createRegistryWithService(year any) (ua *UnitAsset, err error) { - initTemp := initTemplate() - ua, ok := initTemp.(*UnitAsset) +func createRegistryWithService(year any) (ua *UnitAsset, cancel func(), err error) { + sys := createNewSys() + temp, cancel := newResource(createConfAssetMultipleTraits(), &sys) + ua, ok := temp.(*UnitAsset) if !ok { - return nil, fmt.Errorf("Failed while typecasting to local UnitAsset") + return nil, nil, fmt.Errorf("Failed while typecasting to local UnitAsset") } var test forms.ServiceRecord_v1 @@ -594,12 +549,12 @@ func createRegistryWithService(year any) (ua *UnitAsset, err error) { test.IPAddresses = []string{"999.999.999.999"} test.EndOfValidity = fmt.Sprintf("%v-01-02T15:04:05Z", year) ua.serviceRegistry = map[int]forms.ServiceRecord_v1{0: test} - return ua, nil + return ua, cancel, err } type checkExpirationParams struct { servicePresent bool - setup func() (*UnitAsset, error) + setup func() (*UnitAsset, func(), error) testCase string } @@ -607,22 +562,22 @@ func TestCheckExpiration(t *testing.T) { params := []checkExpirationParams{ { true, - func() (ua *UnitAsset, err error) { return createRegistryWithService(2026) }, + func() (ua *UnitAsset, cancel func(), err error) { return createRegistryWithService(2026) }, "Best case, service not past expiration", }, { false, - func() (ua *UnitAsset, err error) { return createRegistryWithService(2006) }, + func() (ua *UnitAsset, cancel func(), err error) { return createRegistryWithService(2006) }, "Bad case, service past expiration", }, { true, - func() (ua *UnitAsset, err error) { return createRegistryWithService("faulty") }, + func() (ua *UnitAsset, cancel func(), err error) { return createRegistryWithService("faulty") }, "Bad case, time parsing problem", }, } for _, c := range params { - ua, err := c.setup() + ua, cancel, err := c.setup() if err != nil { t.Errorf("failed during setup: %v", err) } @@ -634,6 +589,8 @@ func TestCheckExpiration(t *testing.T) { if _, exists := ua.serviceRegistry[0]; (exists == true) && (c.servicePresent == false) { t.Errorf("expected service to be removed in '%s'", c.testCase) } + + cancel() } } From 03489652bd3c61dbf5748cdb9216cee72759a569 Mon Sep 17 00:00:00 2001 From: Pake Date: Wed, 23 Jul 2025 13:11:33 +0200 Subject: [PATCH 68/81] changed errorhandlers in testcases --- esr/thing_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/esr/thing_test.go b/esr/thing_test.go index ecfc36e..1005e68 100644 --- a/esr/thing_test.go +++ b/esr/thing_test.go @@ -184,7 +184,7 @@ func TestServiceRegistryHandlerAdd(t *testing.T) { func(ua *UnitAsset) error { err := sendAddRequest(0, "testDef", "subP", time.Now().Format(time.RFC3339), ua.requests) if err != nil { - return err + t.Fatalf("Failed sending first request") } err = sendAddRequest(1, "testDef2", "subP", time.Now().Format(time.RFC3339), ua.requests) return err @@ -196,7 +196,7 @@ func TestServiceRegistryHandlerAdd(t *testing.T) { func(ua *UnitAsset) error { err := sendAddRequest(0, "testDef", "subP", time.Now().Format(time.RFC3339), ua.requests) if err != nil { - return err + t.Fatalf("Failed sending first request") } err = sendAddRequest(1, "testDef", "subPa", time.Now().Format(time.RFC3339), ua.requests) return err @@ -208,7 +208,7 @@ func TestServiceRegistryHandlerAdd(t *testing.T) { func(ua *UnitAsset) error { err := sendAddRequest(0, "testDef", "subP", time.Now().Format(time.RFC3339), ua.requests) if err != nil { - return err + t.Fatalf("Failed sending first request") } err = sendAddRequest(1, "testDef", "subP", "", ua.requests) return err @@ -221,7 +221,7 @@ func TestServiceRegistryHandlerAdd(t *testing.T) { ch := ua.requests err := sendAddRequest(0, "testDef", "subP", time.Now().Format(time.RFC3339), ch) if err != nil { - return err + t.Fatalf("Failed sending first request") } err = sendAddRequest(1, "testDef", "subP", time.Now().Add(1*time.Hour).Format(time.RFC3339), ch) return err @@ -234,7 +234,7 @@ func TestServiceRegistryHandlerAdd(t *testing.T) { ch := ua.requests err := sendAddRequest(0, "testDef", "subP", time.Now().Format(time.RFC3339), ch) if err != nil { - return err + t.Fatalf("Failed sending first request") } err = sendAddRequest(0, "testDef", "subP", time.Now().Format(time.RFC3339), ch) return err @@ -247,7 +247,7 @@ func TestServiceRegistryHandlerAdd(t *testing.T) { ch := ua.requests err := sendAddRequest(0, "testDef", "subP", time.Now().Format(time.RFC3339), ch) if err != nil { - return err + t.Fatalf("Failed sending first request") } err = sendAddRequest(1, "testDef", "subP", time.Now().Format(time.RFC3339), ch) return err From 84527a6c14b043e513abbdf1c4f7ebf572d21d5b Mon Sep 17 00:00:00 2001 From: walpat-1 Date: Wed, 23 Jul 2025 16:53:19 +0200 Subject: [PATCH 69/81] Fixed PR comments --- esr/scheduler.go | 14 +++++++++++++- esr/scheduler_test.go | 31 +++++++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/esr/scheduler.go b/esr/scheduler.go index 4dd4087..aa6c0c5 100644 --- a/esr/scheduler.go +++ b/esr/scheduler.go @@ -16,22 +16,30 @@ package main -import "time" +import ( + "sync" + "time" +) // Scheduler struct type with the list and three channels type Scheduler struct { taskMap map[int]*time.Timer // list elements has id, timer + mu sync.Mutex } // Returns a scheduler with an empty task map func NewScheduler() *Scheduler { return &Scheduler{ taskMap: make(map[int]*time.Timer), + mu: sync.Mutex{}, } } // AddTask adds a task to the task map and starts a timer for its job, when timer is done it runs the job in a goroutine +// It's up to the caller to ensure that the deadline is not before time.Now() func (s *Scheduler) AddTask(deadline time.Time, job func(), id int) { + s.mu.Lock() + defer s.mu.Unlock() timer, exists := s.taskMap[id] if exists { timer.Stop() @@ -42,6 +50,8 @@ func (s *Scheduler) AddTask(deadline time.Time, job func(), id int) { // RemoveTask removes a scheduled job and deletes the task from the task map func (s *Scheduler) RemoveTask(id int) { + s.mu.Lock() + defer s.mu.Unlock() timer, exists := s.taskMap[id] if !exists { return @@ -52,6 +62,8 @@ func (s *Scheduler) RemoveTask(id int) { // Stop() loops through the task map and turns off the timer for each tasks job func (s *Scheduler) Stop() { + s.mu.Lock() + defer s.mu.Unlock() for _, value := range s.taskMap { value.Stop() } diff --git a/esr/scheduler_test.go b/esr/scheduler_test.go index d0b2122..1553ded 100644 --- a/esr/scheduler_test.go +++ b/esr/scheduler_test.go @@ -12,16 +12,38 @@ import ( func TestAddTask(t *testing.T) { sched := NewScheduler() now := time.Now() + ch := make(chan int) + // case: ensure chrono order + sched.AddTask(now.Add(2*time.Second), func() { ch <- 0 }, 0) + sched.AddTask(now.Add(5*time.Millisecond), func() { ch <- 1 }, 1) - // Add the task - sched.AddTask(now.Add(25*time.Second), func() {}, 0) + select { + case id := <-ch: + if id != 1 { + t.Errorf("Expected 1 from channel, got %d", id) + } + case <-time.After(50 * time.Millisecond): + t.Errorf("Chronological order test timed out") + } + sched.Stop() - if _, exists := sched.taskMap[0]; !exists { - t.Errorf("Task was not present") + sched.AddTask(now.Add(2*time.Second), func() { ch <- 0 }, 0) + sched.AddTask(now.Add(25*time.Millisecond), func() { ch <- 1 }, 0) + + select { + case id := <-ch: + if id != 1 { + t.Errorf("Expected 1 from channel, got %d", id) + } + case <-time.After(50 * time.Millisecond): + t.Errorf("Duplicate ID test timed out") } + sched.Stop() } func TestRemoveTask(t *testing.T) { + // TODO: Similar to AddTask for the PR comment + // Case: task exists sched := NewScheduler() now := time.Now() @@ -45,6 +67,7 @@ func TestRemoveTask(t *testing.T) { } func TestStop(t *testing.T) { + // TODO: Some kind of counter sched := NewScheduler() now := time.Now() From ed709cbea4d6318f22dfddfcc870ffb7b4fee795 Mon Sep 17 00:00:00 2001 From: Pake Date: Fri, 25 Jul 2025 13:27:45 +0200 Subject: [PATCH 70/81] Added returns for RemoveTask() and Stop() --- esr/scheduler.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/esr/scheduler.go b/esr/scheduler.go index aa6c0c5..6273137 100644 --- a/esr/scheduler.go +++ b/esr/scheduler.go @@ -49,23 +49,26 @@ func (s *Scheduler) AddTask(deadline time.Time, job func(), id int) { } // RemoveTask removes a scheduled job and deletes the task from the task map -func (s *Scheduler) RemoveTask(id int) { +func (s *Scheduler) RemoveTask(id int) bool { s.mu.Lock() defer s.mu.Unlock() timer, exists := s.taskMap[id] if !exists { - return + return false } timer.Stop() delete(s.taskMap, id) + return true } // Stop() loops through the task map and turns off the timer for each tasks job -func (s *Scheduler) Stop() { +func (s *Scheduler) Stop() (counter int) { s.mu.Lock() defer s.mu.Unlock() for _, value := range s.taskMap { value.Stop() + counter++ } s.taskMap = make(map[int]*time.Timer) + return } From 59892d637bee7579c5bac5c6fb3500ca5698c3b9 Mon Sep 17 00:00:00 2001 From: Pake Date: Fri, 25 Jul 2025 13:28:16 +0200 Subject: [PATCH 71/81] Finished tests, and comments from PR --- esr/scheduler_test.go | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/esr/scheduler_test.go b/esr/scheduler_test.go index 1553ded..bff955f 100644 --- a/esr/scheduler_test.go +++ b/esr/scheduler_test.go @@ -42,32 +42,30 @@ func TestAddTask(t *testing.T) { } func TestRemoveTask(t *testing.T) { - // TODO: Similar to AddTask for the PR comment - // Case: task exists sched := NewScheduler() now := time.Now() - // Add the task and then remove it + // Add the task and then remove it, function should return true since it removed a task sched.AddTask(now.Add(25*time.Second), func() {}, 0) - sched.RemoveTask(0) + + if removed := sched.RemoveTask(0); removed != true { + t.Errorf("Expected function to return true") + } if _, exists := sched.taskMap[0]; exists { - t.Errorf("Unexpected task exists in taskMap[0]") + t.Errorf("Expected no element in taskMap[0]") } - // Case: task doesn't exist + // Case: task doesn't exist, function should return false since there was no task to remove sched = NewScheduler() // Add the task and then remove it - sched.RemoveTask(0) - - if _, exists := sched.taskMap[0]; exists { - t.Errorf("Unexpected task exists in taskMap[0]") + if removed := sched.RemoveTask(0); removed == true { + t.Errorf("Expected function to return false") } } func TestStop(t *testing.T) { - // TODO: Some kind of counter sched := NewScheduler() now := time.Now() @@ -76,9 +74,9 @@ func TestStop(t *testing.T) { sched.AddTask(now.Add(25*time.Second), func() {}, 1) sched.AddTask(now.Add(25*time.Second), func() {}, 2) sched.AddTask(now.Add(25*time.Second), func() {}, 3) - sched.Stop() + count := sched.Stop() - if len(sched.taskMap) > 0 { - t.Errorf("Expected taskMap to be empty, has %d keys", len(sched.taskMap)) + if count < 4 { + t.Errorf("Expected scheduler to turn off 4 tasks, got %d", count) } } From 0a6f146d8698ac71fa303dafe963c81dd4e9534f Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 25 Jul 2025 14:38:01 +0200 Subject: [PATCH 72/81] Small fixes from the review --- esr/scheduler.go | 4 ++-- esr/scheduler_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esr/scheduler.go b/esr/scheduler.go index 6273137..130861a 100644 --- a/esr/scheduler.go +++ b/esr/scheduler.go @@ -65,8 +65,8 @@ func (s *Scheduler) RemoveTask(id int) bool { func (s *Scheduler) Stop() (counter int) { s.mu.Lock() defer s.mu.Unlock() - for _, value := range s.taskMap { - value.Stop() + for _, timer := range s.taskMap { + timer.Stop() counter++ } s.taskMap = make(map[int]*time.Timer) diff --git a/esr/scheduler_test.go b/esr/scheduler_test.go index bff955f..9962f11 100644 --- a/esr/scheduler_test.go +++ b/esr/scheduler_test.go @@ -13,10 +13,10 @@ func TestAddTask(t *testing.T) { sched := NewScheduler() now := time.Now() ch := make(chan int) + // case: ensure chrono order sched.AddTask(now.Add(2*time.Second), func() { ch <- 0 }, 0) sched.AddTask(now.Add(5*time.Millisecond), func() { ch <- 1 }, 1) - select { case id := <-ch: if id != 1 { @@ -27,9 +27,9 @@ func TestAddTask(t *testing.T) { } sched.Stop() + // Case: ID is reused between tasks sched.AddTask(now.Add(2*time.Second), func() { ch <- 0 }, 0) sched.AddTask(now.Add(25*time.Millisecond), func() { ch <- 1 }, 0) - select { case id := <-ch: if id != 1 { From 609c109bb598d0be3c81611f1e13c08deca743af Mon Sep 17 00:00:00 2001 From: Pake Date: Fri, 25 Jul 2025 20:16:01 +0200 Subject: [PATCH 73/81] Moved and slightly changed the ID check to ensure an old service will re-register with a new ID if registrar restarts --- esr/thing.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/esr/thing.go b/esr/thing.go index 8852e61..b029925 100644 --- a/esr/thing.go +++ b/esr/thing.go @@ -224,6 +224,11 @@ func (ua *UnitAsset) serviceRegistryHandler() { } ua.mu.Lock() // Lock the serviceRegistry map + // Check if the ID exists in the serviceRegistry + if _, exists := ua.serviceRegistry[rec.Id]; !exists { + rec.Id = 0 + } + if rec.Id == 0 { // In the case recCount had looped, check that there is no record at that position for { @@ -245,12 +250,6 @@ func (ua *UnitAsset) serviceRegistryHandler() { log.Printf("The new service %s from system %s has been registered\n", rec.ServiceDefinition, rec.SystemName) } else { // Validate and update existing record - _, exists := ua.serviceRegistry[rec.Id] - if !exists { - ua.mu.Unlock() - request.Error <- fmt.Errorf("no existing record with id %d", rec.Id) - continue - } dbRec := ua.serviceRegistry[rec.Id] if dbRec.ServiceDefinition != rec.ServiceDefinition { request.Error <- errors.New("mismatch between definition received record and database record") From a471f025d7ffb61db7fbbf4ec266d9072f04b18b Mon Sep 17 00:00:00 2001 From: Pake Date: Mon, 28 Jul 2025 12:10:12 +0200 Subject: [PATCH 74/81] Removed unnecessary testcase --- esr/thing_test.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/esr/thing_test.go b/esr/thing_test.go index 1005e68..6a96d1a 100644 --- a/esr/thing_test.go +++ b/esr/thing_test.go @@ -172,13 +172,6 @@ func TestServiceRegistryHandlerAdd(t *testing.T) { func(ua *UnitAsset) error { return sendBrokenAddRequest(0, ua.requests) }, "Bad case, unable to convert to correct form", }, - { - true, - func(ua *UnitAsset) error { - return sendAddRequest(1, "testDef", "subP", time.Now().Format(time.RFC3339), ua.requests) - }, - "Bad case, service doesn't exist in registry", - }, { true, func(ua *UnitAsset) error { From 9cd0864fee39813d69b7f2249cb2bff6d97eba9e Mon Sep 17 00:00:00 2001 From: Pake Date: Mon, 28 Jul 2025 12:32:23 +0200 Subject: [PATCH 75/81] Fixed linter error --- esr/esr_test.go | 21 +++++++--------- esr/thing.go | 66 +++++++++---------------------------------------- 2 files changed, 21 insertions(+), 66 deletions(-) diff --git a/esr/esr_test.go b/esr/esr_test.go index 9a12071..89316ec 100644 --- a/esr/esr_test.go +++ b/esr/esr_test.go @@ -24,10 +24,9 @@ func createLeadingRegistrar() *UnitAsset { Name: "testRegistrar", Details: map[string][]string{"testDetail": {"detail1", "detail2"}}, ServicesMap: components.Services{}, - Traits: Traits{ - leading: true, - leadingSince: time.Now(), - }, + + leading: true, + leadingSince: time.Now(), } return uac } @@ -37,10 +36,9 @@ func createNonLeadingRegistrar() *UnitAsset { Name: "testRegistrar", Details: map[string][]string{"testDetail": {"detail1", "detail2"}}, ServicesMap: components.Services{}, - Traits: Traits{ - leading: false, - leadingRegistrar: &components.CoreSystem{Name: "otherRegistrar", Url: "otherURL"}, - }, + + leading: false, + leadingRegistrar: &components.CoreSystem{Name: "otherRegistrar", Url: "otherURL"}, } return uac } @@ -50,10 +48,9 @@ func createServiceUnavailableRegistrar() *UnitAsset { Name: "testRegistrar", Details: map[string][]string{"testDetail": {"detail1", "detail2"}}, ServicesMap: components.Services{}, - Traits: Traits{ - leading: false, - leadingRegistrar: nil, - }, + + leading: false, + leadingRegistrar: nil, } return uac } diff --git a/esr/thing.go b/esr/thing.go index b029925..293d107 100644 --- a/esr/thing.go +++ b/esr/thing.go @@ -17,7 +17,6 @@ package main import ( - "encoding/json" "errors" "fmt" "log" @@ -41,20 +40,6 @@ type ServiceRegistryRequest struct { Error chan error } -// -------------------------------------Define the unit asset -// Traits are Asset-specific configurable parameters and variables -type Traits struct { - serviceRegistry map[int]forms.ServiceRecord_v1 - - recCount int64 - requests chan ServiceRegistryRequest - // Error chan error // For error handling - sched *Scheduler - leading bool - leadingSince time.Time - leadingRegistrar *components.CoreSystem // if not leading this points to the current leader -} - // UnitAsset type models the unit asset (interface) of the system type UnitAsset struct { Name string `json:"name"` @@ -63,8 +48,14 @@ type UnitAsset struct { ServicesMap components.Services `json:"-"` CervicesMap components.Cervices `json:"-"` // - Traits // Embedding the Traits struct to include its fields and methods - mu sync.Mutex + serviceRegistry map[int]forms.ServiceRecord_v1 + recCount int64 + requests chan ServiceRegistryRequest + sched *Scheduler + leading bool + leadingSince time.Time + leadingRegistrar *components.CoreSystem // if not leading this points to the current leader + mu sync.Mutex } // GetName returns the name of the Resource. @@ -87,11 +78,6 @@ func (ua *UnitAsset) GetDetails() map[string][]string { return ua.Details } -// GetTraits returns the traits of the Resource. -func (ua *UnitAsset) GetTraits() any { - return ua.Traits -} - // ensure UnitAsset implements components.UnitAsset (this check is done at during the compilation) var _ components.UnitAsset = (*UnitAsset)(nil) @@ -128,13 +114,10 @@ func initTemplate() components.UnitAsset { Description: "reports (GET) the role of the Service Registrar as leading or on stand by", } - assetTraits := Traits{} - // Create the UnitAsset with the defined services uat := &UnitAsset{ Name: "registry", Details: map[string][]string{"Location": {"LocalCloud"}}, - Traits: assetTraits, ServicesMap: components.Services{ registerService.SubPath: ®isterService, queryService.SubPath: &queryService, @@ -160,22 +143,10 @@ func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.Sys ServicesMap: usecases.MakeServiceMap(configuredAsset.Services), } - traits, err := UnmarshalTraits(configuredAsset.Traits) - if err != nil { - log.Println("Warning: could not unmarshal traits:", err) - } else if len(traits) > 0 { - ua.Traits = traits[0] // or handle multiple traits if needed - } - - assetTraits := Traits{ - serviceRegistry: make(map[int]forms.ServiceRecord_v1), - recCount: 1, // 0 is used for non registered services - sched: cleaningScheduler, - requests: make(chan ServiceRegistryRequest), // Initialize the requests channel - // Error: make(chan error), // Initialize the error channel - } - - ua.Traits = assetTraits + ua.serviceRegistry = make(map[int]forms.ServiceRecord_v1) + ua.recCount = 1 // 0 is used for non registered services + ua.sched = cleaningScheduler + ua.requests = make(chan ServiceRegistryRequest) // Initialize the requests channel // Start to repeatedly check which is the leading registrar ua.Role() @@ -192,19 +163,6 @@ func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.Sys } } -// UnmarshalTraits unmarshals a slice of json.RawMessage into a slice of Traits. -func UnmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { - var traitsList []Traits - for _, raw := range rawTraits { - var t Traits - if err := json.Unmarshal(raw, &t); err != nil { - return nil, fmt.Errorf("failed to unmarshal trait: %w", err) - } - traitsList = append(traitsList, t) - } - return traitsList, nil -} - //-------------------------------------Unit's resource methods // There are really two assets here: the database and the scheduler From 9801d9d96ce78c960c140967ee4b69543bab960b Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 25 Jul 2025 16:32:10 +0200 Subject: [PATCH 76/81] Adds initial tests for thing.go --- messenger/thing.go | 3 + messenger/thing_test.go | 195 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 messenger/thing_test.go diff --git a/messenger/thing.go b/messenger/thing.go index ad7403f..8fbbabc 100644 --- a/messenger/thing.go +++ b/messenger/thing.go @@ -172,6 +172,9 @@ func (ua *UnitAsset) fetchSystems() (systems []string, err error) { return } body, err := sendRequest("GET", url+"/syslist", nil) + if err != nil { + return + } form, err := usecases.Unpack(body, "application/json") if err != nil { return diff --git a/messenger/thing_test.go b/messenger/thing_test.go new file mode 100644 index 0000000..86b302f --- /dev/null +++ b/messenger/thing_test.go @@ -0,0 +1,195 @@ +package main + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/sdoque/mbaigo/components" +) + +func TestNewRegMsg(t *testing.T) { + table := []struct { + scheme string + expectErr bool + }{ + // Missing both ports + {"", true}, + // Having http port + {"http", false}, + // Having https port + {"https", false}, + } + + sys := components.NewSystem("test sys", context.Background()) + sys.Husk = &components.Husk{ + ProtoPort: map[string]int{"https": 0, "http": 0}, + } + for _, test := range table { + sys.Husk.ProtoPort[test.scheme] = 8080 + body, err := newRegMsg(&sys) + + if got, want := err != nil, test.expectErr; got != want { + t.Errorf("expected error %v, got: %v", want, err) + } + + if got, want := len(body) < 1, test.expectErr; got != want { + t.Errorf("expected body %v, got: %s", want, string(body)) + } + if got, want := bytes.Contains(body, []byte(test.scheme)), true; got != want { + t.Errorf("expected URL scheme %v in body, but it's missing", test.scheme) + } + } +} + +type transSendRequest struct { + errResponse error + status int + body io.Reader +} + +func newTransSendRequest() *transSendRequest { + lt := &transSendRequest{} + http.DefaultClient.Transport = lt + return lt +} + +// This mock transport also verifies that the system message forms are valid. +func (mock *transSendRequest) RoundTrip(req *http.Request) (*http.Response, error) { + defer req.Body.Close() + if mock.errResponse != nil { + return nil, mock.errResponse + } + rec := httptest.NewRecorder() + rec.WriteHeader(mock.status) + res := rec.Result() + res.Body = io.NopCloser(mock.body) + return res, nil +} + +var errMock = fmt.Errorf("mock error") +var bodyMock = "body ok" + +func TestSendRequest(t *testing.T) { + table := []struct { + method string + err error + status int + body io.Reader + expectErr bool + }{ + // Bad method + {"no method", nil, 0, nil, true}, + // Error from defaultclient + {http.MethodGet, errMock, 0, nil, true}, + // Bad status code + {http.MethodGet, nil, http.StatusInternalServerError, nil, true}, + // Error from body + {http.MethodGet, nil, http.StatusOK, &errorReader{}, true}, + // All ok + {http.MethodGet, nil, http.StatusOK, strings.NewReader(bodyMock), false}, + } + + mock := newTransSendRequest() + for _, test := range table { + mock.errResponse = test.err + mock.status = test.status + mock.body = test.body + body, err := sendRequest(test.method, "/test/url", nil) + + if got, want := err != nil, test.expectErr; got != want { + t.Errorf("expected error %v, got: %v", want, err) + } + if !test.expectErr && string(body) != bodyMock { + t.Errorf("expected body '%s', got '%s'", bodyMock, string(body)) + } + } +} + +type transFetchSystems struct { + t *testing.T + coreStatus int + reqStatus int + body string +} + +func newTransFetchSystems(t *testing.T) *transFetchSystems { + lt := &transFetchSystems{t: t} + http.DefaultClient.Transport = lt + return lt +} + +// This mock transport also verifies that the system message forms are valid. +func (mock *transFetchSystems) RoundTrip(req *http.Request) (*http.Response, error) { + if req.Body != nil { + req.Body.Close() + } + rec := httptest.NewRecorder() + switch req.URL.Path { + case "/status": + rec.WriteHeader(mock.coreStatus) + if mock.coreStatus == http.StatusOK { + fmt.Fprint(rec.Body, components.ServiceRegistrarLeader) + } + case "/syslist": + rec.WriteHeader(mock.reqStatus) + fmt.Fprint(rec.Body, mock.body) + default: + mock.t.Errorf("unexpected path: %s", req.URL.Path) + rec.WriteHeader(http.StatusInternalServerError) + } + return rec.Result(), nil +} + +func TestFetchSystems(t *testing.T) { + table := []struct { + coreStatus int + reqStatus int + expectErr bool + body string + }{ + // Error from GetRunningCoreSystemURL + {http.StatusInternalServerError, 0, true, ""}, + // Error from sendRequest + {http.StatusOK, http.StatusInternalServerError, true, ""}, + // Error from Unpack + {http.StatusOK, http.StatusOK, true, ""}, + // Error from form + {http.StatusOK, http.StatusOK, true, `{"version":"MessengerRegistration_v1"}`}, + // All ok + {http.StatusOK, http.StatusOK, false, + `{"version":"SystemRecordList_v1", "systemurl":["http://test"]}`, + }, + } + + sys := components.NewSystem("test sys", context.Background()) + sys.CoreS = []*components.CoreSystem{ + {Name: "serviceregistrar", Url: "http://fake"}, + } + ua := &UnitAsset{ + Owner: &sys, + } + mock := newTransFetchSystems(t) + + for _, test := range table { + mock.coreStatus = test.coreStatus + mock.reqStatus = test.reqStatus + mock.body = test.body + list, err := ua.fetchSystems() + + if got, want := err != nil, test.expectErr; got != want { + t.Errorf("expected error %v, got: %v", want, err) + } + if err != nil { + continue + } + if got, want := len(list), 1; got != want { + t.Errorf("expected %d urls in list, got %d", want, got) + } + } +} From 7152e0c70228e06036eb8a2cbcb1b0b491adbb96 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 8 Aug 2025 12:41:20 +0200 Subject: [PATCH 77/81] Avoid periodically spamming warnings for a minor error --- messenger/thing.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messenger/thing.go b/messenger/thing.go index 8fbbabc..fea598a 100644 --- a/messenger/thing.go +++ b/messenger/thing.go @@ -134,7 +134,7 @@ func (ua *UnitAsset) runBeacon() { for { systems, err := ua.fetchSystems() if err != nil { - usecases.LogWarn(ua.Owner, "error fetching system list: %s", err) + usecases.LogInfo(ua.Owner, "error fetching system list: %s", err) } ua.notifySystems(systems) select { From c5ddf5120b95b43f19eda2687b5ea43112d7a82c Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 8 Aug 2025 12:41:20 +0200 Subject: [PATCH 78/81] Finishes the tests for messenger --- messenger/thing.go | 5 ++++- messenger/thing_test.go | 48 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/messenger/thing.go b/messenger/thing.go index fea598a..14faefb 100644 --- a/messenger/thing.go +++ b/messenger/thing.go @@ -157,10 +157,10 @@ func sendRequest(method, url string, body []byte) ([]byte, error) { if err != nil { return nil, err } + defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode > 299 { return nil, fmt.Errorf("bad response: %s", resp.Status) } - defer resp.Body.Close() return io.ReadAll(resp.Body) } @@ -219,6 +219,7 @@ func (ua *UnitAsset) addMessage(msg forms.SystemMessage_v1) { body: msg.Body, }) if len(ua.messages[msg.System]) > maxMessages { + // Strips the oldest msg from the front of the slice ua.messages[msg.System] = ua.messages[msg.System][1:] } } @@ -227,6 +228,8 @@ func (ua *UnitAsset) addMessage(msg forms.SystemMessage_v1) { // The log is appended to in a chronological order already, so the latest error // and warning for each system will be returned and "all" will be in reverse // chronological order. +// NOTE: No tests are provided for this function, as it's most likely subject +// to later changes. func (ua *UnitAsset) filterLogs() (errors, warnings map[string]message, all []message) { errors = make(map[string]message) warnings = make(map[string]message) diff --git a/messenger/thing_test.go b/messenger/thing_test.go index 86b302f..bd75ce0 100644 --- a/messenger/thing_test.go +++ b/messenger/thing_test.go @@ -11,6 +11,7 @@ import ( "testing" "github.com/sdoque/mbaigo/components" + "github.com/sdoque/mbaigo/forms" ) func TestNewRegMsg(t *testing.T) { @@ -193,3 +194,50 @@ func TestFetchSystems(t *testing.T) { } } } + +func TestNotifySystems(t *testing.T) { + name := "test messenger" + urls := []string{ + "\x00bad", // Bad URLs + "/" + name, // Skip itself + "/good", // All ok + } + sys := components.NewSystem(name, context.Background()) + ua := &UnitAsset{ + Owner: &sys, + } + mock := newTransSendRequest() + mock.status = http.StatusOK + mock.body = strings.NewReader("ok") // Required for sendRequest() + for _, test := range urls { + ua.notifySystems([]string{test}) + } +} + +func TestAddMessage(t *testing.T) { + sys := "test" + ua := &UnitAsset{ + messages: make(map[string][]message), + } + for i := range maxMessages * 2 { + msg := forms.SystemMessage_v1{ + Level: forms.LevelDebug, + System: sys, + Body: fmt.Sprintf("%d", i), + } + ua.addMessage(msg) + } + + size := len(ua.messages[sys]) + if got, want := size, maxMessages; got > want { + t.Errorf("expected max messages %d, got %d", want, got) + } + oldest := ua.messages[sys][0] + if got, want := oldest.body, fmt.Sprintf("%d", maxMessages); got != want { + t.Errorf("expected oldest msg '%s', got '%s'", want, got) + } + newest := ua.messages[sys][size-1] + if got, want := newest.body, fmt.Sprintf("%d", maxMessages*2-1); got != want { + t.Errorf("expected newest msg '%s', got '%s'", want, got) + } +} From 14cc890a71c1c41f02b566aecd59716666c64ce6 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 8 Aug 2025 13:57:53 +0200 Subject: [PATCH 79/81] Removes unrelated go.mod files --- esr/go.mod | 8 -------- esr/go.sum | 0 orchestrator/go.mod | 8 -------- orchestrator/go.sum | 0 4 files changed, 16 deletions(-) delete mode 100644 esr/go.mod delete mode 100644 esr/go.sum delete mode 100644 orchestrator/go.mod delete mode 100644 orchestrator/go.sum diff --git a/esr/go.mod b/esr/go.mod deleted file mode 100644 index 189d0ad..0000000 --- a/esr/go.mod +++ /dev/null @@ -1,8 +0,0 @@ -module github.com/sdoque/systems/esr - -go 1.24.4 - -require github.com/sdoque/mbaigo v0.0.0-20250520155324-7390c339652a - -// Replaces this library with a patched version -replace github.com/sdoque/mbaigo v0.0.0-20250520155324-7390c339652a => /home/lmas/code/mbaigo diff --git a/esr/go.sum b/esr/go.sum deleted file mode 100644 index e69de29..0000000 diff --git a/orchestrator/go.mod b/orchestrator/go.mod deleted file mode 100644 index 480b6a9..0000000 --- a/orchestrator/go.mod +++ /dev/null @@ -1,8 +0,0 @@ -module github.com/sdoque/systems/orchestrator - -go 1.24.4 - -require github.com/sdoque/mbaigo v0.0.0-20250520155324-7390c339652a - -// Replaces this library with a patched version -replace github.com/sdoque/mbaigo v0.0.0-20250520155324-7390c339652a => /home/lmas/code/mbaigo diff --git a/orchestrator/go.sum b/orchestrator/go.sum deleted file mode 100644 index e69de29..0000000 From 282a8511261922082047415fe345d426ee4e2e1b Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 8 Aug 2025 14:00:26 +0200 Subject: [PATCH 80/81] Revert "TODO: temporary testing logging for ds18 system, COMMIT TO BE REMOVED" This reverts commit 11d62971b76e76b03c1ab700e759277b7a0a4f92. --- ds18b20/ds18b20.go | 19 ++++++++++--------- ds18b20/thing.go | 14 ++++++++------ 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/ds18b20/ds18b20.go b/ds18b20/ds18b20.go index 3f82c00..12c420a 100644 --- a/ds18b20/ds18b20.go +++ b/ds18b20/ds18b20.go @@ -20,6 +20,8 @@ import ( "context" "crypto/x509/pkix" "encoding/json" + "fmt" + "log" "net/http" "time" @@ -60,15 +62,14 @@ func main() { // Configure the system rawResources, err := usecases.Configure(&sys) if err != nil { - usecases.LogWarn(&sys, "configuration error: %v", err) + log.Fatalf("configuration error: %v\n", err) } sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) var cleanups []func() for _, raw := range rawResources { var uac usecases.ConfigurableAsset if err := json.Unmarshal(raw, &uac); err != nil { - usecases.LogError(&sys, "resource configuration error: %+v", err) - return + log.Fatalf("resource configuration error: %+v\n", err) } ua, cleanup := newResource(uac, &sys) cleanups = append(cleanups, cleanup) @@ -87,7 +88,7 @@ func main() { // wait for shutdown signal, and gracefully close properly goroutines with context <-sys.Sigs // wait for a SIGINT (Ctrl+C) signal - usecases.LogInfo(&sys, "shuting down system %s", sys.Name) + log.Println("\nshuting down system", sys.Name) cancel() // cancel the context, signaling the goroutines to stop time.Sleep(2 * time.Second) // allow the go routines to be executed, which might take more time than the main routine to end } @@ -114,18 +115,18 @@ func (ua *UnitAsset) readTemp(w http.ResponseWriter, r *http.Request) { ua.trayChan <- getMeasuremet select { case err := <-getMeasuremet.Error: - usecases.LogError(ua.Owner, "error getting measurement: %v", err) - w.WriteHeader(http.StatusInternalServerError) + fmt.Printf("Logic error in getting measurement, %s\n", err) + w.WriteHeader(http.StatusInternalServerError) // Use 500 for an internal error return case temperatureForm := <-getMeasuremet.ValueP: usecases.HTTPProcessGetRequest(w, r, &temperatureForm) return case <-time.After(5 * time.Second): // Optional timeout - usecases.LogWarn(ua.Owner, "timed out while getting measurement") - http.Error(w, "Measurement timed out", http.StatusInternalServerError) + http.Error(w, "Request timed out", http.StatusGatewayTimeout) + log.Println("Failure to process temperature reading request") return } default: - http.Error(w, "Method is not supported.", http.StatusMethodNotAllowed) + http.Error(w, "Method is not supported.", http.StatusNotFound) } } diff --git a/ds18b20/thing.go b/ds18b20/thing.go index 34befbd..7306b97 100644 --- a/ds18b20/thing.go +++ b/ds18b20/thing.go @@ -20,6 +20,7 @@ import ( "context" "encoding/json" "fmt" + "log" "os" "strconv" "strings" @@ -122,7 +123,7 @@ func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.Sys traits, err := UnmarshalTraits(configuredAsset.Traits) if err != nil { - usecases.LogWarn(sys, "could not unmarshal traits: %v", err) + log.Println("Warning: could not unmarshal traits:", err) } else if len(traits) > 0 { ua.Traits = traits[0] // or handle multiple traits if needed } @@ -130,7 +131,7 @@ func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.Sys go ua.readTemperature(sys.Ctx) return ua, func() { - usecases.LogInfo(sys, "disconnecting from %s", ua.Name) + log.Printf("disconnecting from %s\n", ua.Name) } } @@ -171,25 +172,25 @@ func (ua *UnitAsset) readTemperature(ctx context.Context) { deviceFile := "/sys/bus/w1/devices/" + ua.Name + "/w1_slave" rawData, err := os.ReadFile(deviceFile) if err != nil { - usecases.LogError(ua.Owner, "Error reading temperature file: %s, error: %v", deviceFile, err) + log.Printf("Error reading temperature file: %s, error: %v\n", deviceFile, err) continue // Retry on the next cycle } if len(rawData) == 0 { - usecases.LogWarn(ua.Owner, "Empty data read from temperature file: %s", deviceFile) + log.Printf("Empty data read from temperature file: %s\n", deviceFile) continue } rawValue := strings.Split(string(rawData), "\n")[1] if !strings.Contains(rawValue, "t=") { - usecases.LogError(ua.Owner, "Invalid temperature data: %s", rawData) + log.Printf("Invalid temperature data: %s\n", rawData) continue } tempStr := strings.Split(rawValue, "t=")[1] temp, err := strconv.ParseFloat(tempStr, 64) if err != nil { - usecases.LogError(ua.Owner, "Error parsing temperature: %v", err) + log.Printf("Error parsing temperature: %v\n", err) continue } @@ -207,6 +208,7 @@ func (ua *UnitAsset) readTemperature(ctx context.Context) { for { select { case <-ctx.Done(): // Shutdown + log.Println("Context canceled, stopping temperature readings.") return case temp := <-tempChan: // Update temperature and timestamp From c96303990db77875994db2ce77d400b5f92382b3 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 8 Aug 2025 14:03:40 +0200 Subject: [PATCH 81/81] Removes more go.mod files --- ds18b20/go.mod | 8 -------- ds18b20/go.sum | 0 2 files changed, 8 deletions(-) delete mode 100644 ds18b20/go.mod delete mode 100644 ds18b20/go.sum diff --git a/ds18b20/go.mod b/ds18b20/go.mod deleted file mode 100644 index 3e5865a..0000000 --- a/ds18b20/go.mod +++ /dev/null @@ -1,8 +0,0 @@ -module github.com/sdoque/systems/ds18b20 - -go 1.24.4 - -require github.com/sdoque/mbaigo v0.0.0-20250520155324-7390c339652a - -// Replaces this library with a patched version -replace github.com/sdoque/mbaigo v0.0.0-20250520155324-7390c339652a => /home/lmas/code/mbaigo diff --git a/ds18b20/go.sum b/ds18b20/go.sum deleted file mode 100644 index e69de29..0000000