diff --git a/.gitignore b/.gitignore index 025442c..b26334e 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,5 @@ serviceRegistry.db *.pem **/files/ *.pdf +*.xlsx diff --git a/README.md b/README.md index 4bd1858..01bb895 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ -# mbaigo systems +# mbaigo systems as building blocks The Arrowhead Framework is used to compose a *system of systems* for a specific purpose. +At the same time, a system of systems is a system itself, with emerging behaviors. (Synecdoque is a figure of speech in which a part is made to represent the whole or vice versa.) It is similar to building with LEGO: the same set of building blocks can be assembled into different solutions (e.g., a plane or a car), and the final assembly represents a distinct functional concept. For example, to build a climate control solution, you would select and integrate systems such as a temperature sensor system, a valve (actuator) system, and a thermostat/control system. diff --git a/beehive/go.mod b/beehive/go.mod index 621f5a5..fecb9ce 100644 --- a/beehive/go.mod +++ b/beehive/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/beehive -go 1.26.2 +go 1.26.3 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/beehive/thing.go b/beehive/thing.go index 9dd387b..ad2d09d 100644 --- a/beehive/thing.go +++ b/beehive/thing.go @@ -189,7 +189,8 @@ func (t *Traits) backgroundPoll() { // fetchOnOffState retrieves the current boolean value from one on_off service URL. func fetchOnOffState(url string) (state bool, online bool) { - client := &http.Client{Timeout: 3 * time.Second} + // Preserve framework-installed TLS so this works against HTTPS-only peers. + client := &http.Client{Timeout: 3 * time.Second, Transport: http.DefaultClient.Transport} resp, err := client.Get(url) if err != nil { return false, false @@ -290,7 +291,8 @@ func toggleHandler(t *Traits, w http.ResponseWriter, r *http.Request, ctx contex } req.Header.Set("Content-Type", "application/json") - client := &http.Client{Timeout: 5 * time.Second} + // Preserve framework-installed TLS so this works against HTTPS-only peers. + client := &http.Client{Timeout: 5 * time.Second, Transport: http.DefaultClient.Transport} resp, err := client.Do(req) if err != nil { http.Error(w, "upstream error: "+err.Error(), http.StatusBadGateway) diff --git a/beekeeper/beekeeper.go b/beekeeper/beekeeper.go index 7a96187..87f5a71 100644 --- a/beekeeper/beekeeper.go +++ b/beekeeper/beekeeper.go @@ -169,7 +169,10 @@ func setLightState(cfg DeconzConfig, lightID string, on bool) error { return err } req.Header.Set("Content-Type", "application/json") - resp, err := (&http.Client{Timeout: 5 * time.Second}).Do(req) + // Preserve the framework's TLS transport while imposing a timeout — + // http.DefaultClient itself has no timeout. Without Transport set the + // fresh client would lose mTLS configuration installed by enrolment. + resp, err := (&http.Client{Timeout: 5 * time.Second, Transport: http.DefaultClient.Transport}).Do(req) if err != nil { return err } diff --git a/beekeeper/go.mod b/beekeeper/go.mod index af3f7c7..74a9da9 100644 --- a/beekeeper/go.mod +++ b/beekeeper/go.mod @@ -1,6 +1,6 @@ module github.com/sdoque/systems/beekeeper -go 1.26.2 +go 1.26.3 require ( github.com/gorilla/websocket v1.5.3 diff --git a/busdriver/go.mod b/busdriver/go.mod index 4d8f007..e6e0c56 100644 --- a/busdriver/go.mod +++ b/busdriver/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/busdriver -go 1.26.2 +go 1.26.3 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/ca/go.mod b/ca/go.mod index 763fdf6..639894d 100644 --- a/ca/go.mod +++ b/ca/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/ca -go 1.26.2 +go 1.26.3 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/ca/hostkeygen b/ca/hostkeygen deleted file mode 100755 index e872fa9..0000000 Binary files a/ca/hostkeygen and /dev/null differ diff --git a/ca/thing.go b/ca/thing.go index 0b74cf6..6b5f3a2 100644 --- a/ca/thing.go +++ b/ca/thing.go @@ -325,12 +325,18 @@ func generateSelfSignedCert(sys *components.System) ([]byte, []byte, error) { Organization: []string{"Synecdoque"}, CommonName: "synecdoque.com", }, - DNSNames: dnsNames, - IPAddresses: ipAddrs, - NotBefore: notBefore, - NotAfter: notAfter, - KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + DNSNames: dnsNames, + IPAddresses: ipAddrs, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + // ExtKeyUsage on the CA root must include every purpose the certs it + // signs will be used for. End-entity system certs in this cloud carry + // both ServerAuth and ClientAuth (they serve mTLS *and* call mTLS). + // Per RFC 5280, an issuer's ExtKeyUsage must be a superset of the + // cert it signs. Without ClientAuth here, every mTLS handshake fails + // with "x509: certificate specifies an incompatible key usage". + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, BasicConstraintsValid: true, IsCA: true, } diff --git a/clerk/go.mod b/clerk/go.mod index 94a38ff..211198f 100644 --- a/clerk/go.mod +++ b/clerk/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/clerk -go 1.26.2 +go 1.26.3 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/clerk/thing.go b/clerk/thing.go index 0f24a81..cf9acc8 100644 --- a/clerk/thing.go +++ b/clerk/thing.go @@ -229,7 +229,8 @@ func (t *Traits) lookupFromTracker(w http.ResponseWriter, id, email string) { } targetURL := baseURL + "?id=" + url.QueryEscape(id) + "&email=" + url.QueryEscape(email) - resp, err := (&http.Client{Timeout: 10 * time.Second}).Get(targetURL) + // Preserve framework-installed TLS so this works against an HTTPS-only tracker. + resp, err := (&http.Client{Timeout: 10 * time.Second, Transport: http.DefaultClient.Transport}).Get(targetURL) if err != nil { cer.Nodes = make(map[string][]components.NodeInfo) http.Error(w, "tracker error: "+err.Error(), http.StatusBadGateway) diff --git a/collector/go.mod b/collector/go.mod index 882ad67..1ff1a2f 100644 --- a/collector/go.mod +++ b/collector/go.mod @@ -1,6 +1,6 @@ module github.com/sdoque/systems/collector -go 1.26.2 +go 1.26.3 require ( github.com/influxdata/influxdb-client-go/v2 v2.14.0 diff --git a/democrat/go.mod b/democrat/go.mod index 73680de..7f22985 100644 --- a/democrat/go.mod +++ b/democrat/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/democrat -go 1.26.2 +go 1.26.3 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/drafter/go.mod b/drafter/go.mod index b40a049..f315974 100644 --- a/drafter/go.mod +++ b/drafter/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/drafter -go 1.26.2 +go 1.26.3 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/ds18b20/go.mod b/ds18b20/go.mod index 70fa324..e71b46e 100644 --- a/ds18b20/go.mod +++ b/ds18b20/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/ds18b20 -go 1.26.2 +go 1.26.3 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/emulator/go.mod b/emulator/go.mod index c38184c..d7f2b3b 100644 --- a/emulator/go.mod +++ b/emulator/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/emulator -go 1.26.2 +go 1.26.3 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/esr/go.mod b/esr/go.mod index 97f8f14..51f0687 100644 --- a/esr/go.mod +++ b/esr/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/esr -go 1.26.2 +go 1.26.3 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/ethermostat/go.mod b/ethermostat/go.mod index d8b55b7..a9d6e01 100644 --- a/ethermostat/go.mod +++ b/ethermostat/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/ethermostat -go 1.26.2 +go 1.26.3 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/filmer/go.mod b/filmer/go.mod index 8dd0def..a948579 100644 --- a/filmer/go.mod +++ b/filmer/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/filmer -go 1.26.2 +go 1.26.3 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/flattener/go.mod b/flattener/go.mod index 843d892..33b12b6 100644 --- a/flattener/go.mod +++ b/flattener/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/flattener -go 1.26.2 +go 1.26.3 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/kgrapher/go.mod b/kgrapher/go.mod index 5b59c82..c1e1379 100644 --- a/kgrapher/go.mod +++ b/kgrapher/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/kgrapher -go 1.26.2 +go 1.26.3 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/kgrapher/thing.go b/kgrapher/thing.go index 1143520..b91a1f4 100644 --- a/kgrapher/thing.go +++ b/kgrapher/thing.go @@ -183,7 +183,9 @@ func (t *Traits) assembleOntologies(w http.ResponseWriter) { } req = req.WithContext(ctx) - client := &http.Client{} + // Use the framework's TLS-configured transport so this call works + // against an HTTPS-only service registrar. + client := &http.Client{Transport: http.DefaultClient.Transport} resp, err := client.Do(req) if err != nil { log.Printf("Error receiving the systems list from service registrar, %s\n", err) @@ -342,7 +344,10 @@ func (t *Traits) assembleOntologies(w http.ResponseWriter) { } req.Header.Set("Content-Type", "text/turtle") - client = &http.Client{} + // Triple-store endpoint is typically HTTP-only on localhost, but use the + // framework transport for consistency in case the deployment ever moves + // it to HTTPS. + client = &http.Client{Transport: http.DefaultClient.Transport} resp, err = client.Do(req) if err != nil { log.Println("Error PUTting snapshot:", err) diff --git a/leveler/go.mod b/leveler/go.mod index 946ff33..6293206 100644 --- a/leveler/go.mod +++ b/leveler/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/leveler -go 1.26.2 +go 1.26.3 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/maitreD/go.mod b/maitreD/go.mod index e5d4f02..84a58f6 100644 --- a/maitreD/go.mod +++ b/maitreD/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/maitreD -go 1.26.2 +go 1.26.3 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/messenger/go.mod b/messenger/go.mod index e8717ef..f648288 100644 --- a/messenger/go.mod +++ b/messenger/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/messenger -go 1.26.2 +go 1.26.3 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/meteorologue/go.mod b/meteorologue/go.mod index 6271591..b4e901f 100644 --- a/meteorologue/go.mod +++ b/meteorologue/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/meteorologue -go 1.26.2 +go 1.26.3 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/modboss/go.mod b/modboss/go.mod index 1b19bd8..9458af3 100644 --- a/modboss/go.mod +++ b/modboss/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/modboss -go 1.26.2 +go 1.26.3 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/modeler/go.mod b/modeler/go.mod index 7d4cc28..64bd16d 100644 --- a/modeler/go.mod +++ b/modeler/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/modeler -go 1.26.2 +go 1.26.3 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/modeler/thing.go b/modeler/thing.go index 1f568e7..14a5b23 100644 --- a/modeler/thing.go +++ b/modeler/thing.go @@ -142,7 +142,10 @@ func (t *Traits) assembleModel(w http.ResponseWriter) { } req = req.WithContext(ctx) - client := &http.Client{} + // Preserve the framework-configured Transport so this call uses mTLS + // when the registrar serves only HTTPS. http.DefaultClient.Transport is + // set up by installTLSConfig at enrolment. + client := &http.Client{Transport: http.DefaultClient.Transport} resp, err := client.Do(req) if err != nil { log.Printf("Error fetching system list: %s\n", err) diff --git a/nurse/go.mod b/nurse/go.mod index 34ac2f7..6f3d4f2 100644 --- a/nurse/go.mod +++ b/nurse/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/nurse -go 1.26.2 +go 1.26.3 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/nurse/thing.go b/nurse/thing.go index 39c06e1..eedc8c8 100644 --- a/nurse/thing.go +++ b/nurse/thing.go @@ -39,13 +39,16 @@ import ( // -------------------------------------Define a measurement (or signal) type SignalT struct { - Name string `json:"serviceDefinition"` - Details map[string][]string `json:"details"` - Period time.Duration `json:"samplingPeriod"` - Threshold float64 `json:"threshold"` - TOverCount map[string]int `json:"-"` // consecutive over-threshold count per source node - WorkRequested map[string]bool `json:"-"` // pending maintenance order per source node - Operational bool `json:"-"` // false when any node has a pending order + Name string `json:"serviceDefinition"` + Details map[string][]string `json:"details"` + Period time.Duration `json:"samplingPeriod"` + LowerThreshold float64 `json:"lowerThreshold"` + UpperThreshold float64 `json:"upperThreshold"` + TOverCount map[string]int `json:"-"` // consecutive out-of-range count per source node + WorkRequested map[string]bool `json:"-"` // pending maintenance order per source node + Operational bool `json:"-"` // false when any node has a pending order + ValveTagByNode map[string]string `json:"-"` // node → actuator FL tag resolved from GraphDB + UnresolvableNodes map[string]bool `json:"-"` // nodes whose actuator could not be resolved; skipped } //-------------------------------------Define the unit asset @@ -53,6 +56,7 @@ type SignalT struct { // Traits holds the configurable parameters for the nurse unit asset. type Traits struct { SAP_URL string `json:"sap_url"` + GraphDB_URL string `json:"graphdb_url"` Signals []SignalT `json:"signals"` pendingOrders map[string]string // orderID → signalName; not serialized owner *components.System @@ -78,14 +82,16 @@ func initTemplate() *components.UnitAsset { monitorService.SubPath: &monitorService, }, Traits: &Traits{ - SAP_URL: "http://192.168.1.108:20191/sapper/SAPSimulator/orders", + SAP_URL: "http://192.168.1.108:20191/sapper/SAPSimulator/orders", + GraphDB_URL: "http://13.79.36.131:7200/repositories/arrowhead-skoghall-v2", Signals: []SignalT{ { - Name: "temperature", - Details: map[string][]string{"Unit": {"Celsius"}}, - Period: 4, - Threshold: 75.0, - Operational: true, + Name: "pressure", + Details: map[string][]string{"Unit": {"kPa"}}, + Period: 4, + LowerThreshold: 10.0, + UpperThreshold: 25.0, + Operational: true, }, }, }, @@ -112,6 +118,8 @@ func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.Sys t.Signals[i].Operational = true t.Signals[i].TOverCount = make(map[string]int) t.Signals[i].WorkRequested = make(map[string]bool) + t.Signals[i].ValveTagByNode = make(map[string]string) + t.Signals[i].UnresolvableNodes = make(map[string]bool) } // Derive the health endpoint from the SAP URL (replace the API path with /health) @@ -127,6 +135,17 @@ func newResource(configuredAsset usecases.ConfigurableAsset, sys *components.Sys fmt.Printf("SAP server is down (%v, in %s)\n", r.Err, r.Duration) } + // GraphDB is load-bearing: without it the nurse cannot resolve a sensor + // to its associated asset and any maintenance order it raised would be + // misdirected. Refuse to start rather than emit confused work orders. + if t.GraphDB_URL == "" { + log.Fatalf("nurse: graphdb_url is required in configuration") + } + if err := CheckGraphDBUp(t.GraphDB_URL, 5*time.Second); err != nil { + log.Fatalf("nurse: GraphDB unreachable at %s: %v", t.GraphDB_URL, err) + } + log.Printf("GraphDB reachable at %s", t.GraphDB_URL) + sProtocols := components.SProtocols(sys.Husk.ProtoPort) cervices := make(components.Cervices) for _, signal := range t.Signals { @@ -189,7 +208,7 @@ func UnmarshalTraits(rawTraits []json.RawMessage) ([]Traits, error) { //-------------------------------------Unit asset's function methods // sigMon periodically monitors all providers of a signal and requests maintenance -// when any single source exceeds the threshold 5 consecutive times. +// when any single source stays outside the [lower, upper] range 5 consecutive times. func (t *Traits) sigMon(name string, period time.Duration) error { ticker := time.NewTicker(period) defer ticker.Stop() @@ -218,12 +237,22 @@ func (t *Traits) sigMon(name string, period time.Duration) error { log.Printf("Discovered %d provider(s) for %s\n", len(cer.Nodes), name) } + // Resolve the actuator (e.g. valve) each provider's sensor diagnoses. + // Done here, before polling, so a missing diagnosesActuator relationship + // surfaces during normal operation rather than under an anomaly. + t.resolveActuators(sig, cer.Nodes) + // Query each provider individually to preserve its identity for per-source counting. failed := false for node, nodeInfos := range cer.Nodes { if failed { break } + // Skip nodes whose actuator could not be resolved — sending an order + // for a misidentified asset is worse than sending no order at all. + if sig.UnresolvableNodes[node] { + continue + } for _, ni := range nodeInfos { // Skip this node while its maintenance order is pending. if sig.WorkRequested[node] { @@ -251,19 +280,18 @@ func (t *Traits) sigMon(name string, period time.Duration) error { log.Printf("Measurement: %s from %s, Value: %.2f, Time: %s\n", name, node, tup.Value, time.Now().Format(time.RFC3339)) - if tup.Value > sig.Threshold { + if tup.Value < sig.LowerThreshold || tup.Value > sig.UpperThreshold { sig.TOverCount[node]++ - log.Printf("ALERT: %s/%s value %.2f exceeds threshold %.2f (count: %d/5)\n", - name, node, tup.Value, sig.Threshold, sig.TOverCount[node]) + log.Printf("ALERT: %s/%s value %.2f outside range [%.2f, %.2f] (count: %d/5)\n", + name, node, tup.Value, sig.LowerThreshold, sig.UpperThreshold, sig.TOverCount[node]) if sig.TOverCount[node] >= 5 { sig.Operational = false sig.WorkRequested[node] = true log.Printf("Signal %s/%s non-operational, requesting maintenance\n", name, node) equipmentID := assetNameFromURL(ni.URL) - location := "" - if locs, ok := ni.Details["FunctionalLocation"]; ok && len(locs) > 0 { - location = locs[0] - } + // The actuator FL tag resolved from GraphDB is the SAP + // dispatch target — the sensor only diagnoses the fault. + location := sig.ValveTagByNode[node] orderID := t.requestMaintenanceOrder(sig, equipmentID, location) if orderID != "" { t.pendingOrders[orderID] = name @@ -273,7 +301,7 @@ func (t *Traits) sigMon(name string, period time.Duration) error { } } } else if sig.TOverCount[node] > 0 { - log.Printf("Signal %s/%s back below threshold (resetting count from %d)\n", + log.Printf("Signal %s/%s back in range (resetting count from %d)\n", name, node, sig.TOverCount[node]) sig.TOverCount[node] = 0 } @@ -301,8 +329,8 @@ func (t *Traits) state(w http.ResponseWriter) { if pending == "" { pending = " none" } - text += fmt.Sprintf("Signal: %s, Threshold: %f, TOverCount:[%s], Operational: %t, WorkRequested:[%s]\n", - signal.Name, signal.Threshold, counts, signal.Operational, pending) + text += fmt.Sprintf("Signal: %s, Range: [%.2f, %.2f], TOverCount:[%s], Operational: %t, WorkRequested:[%s]\n", + signal.Name, signal.LowerThreshold, signal.UpperThreshold, counts, signal.Operational, pending) } w.Write([]byte(text)) } @@ -425,6 +453,144 @@ func CheckServerUp(rawURL string, timeout time.Duration) CheckResult { return CheckResult{Up: true, StatusCode: resp.StatusCode, Duration: time.Since(start)} } +// resolveActuators looks up the actuator (e.g. valve) functional-location tag +// for every newly-discovered node in nodes. The result is cached on the signal +// so each sensor pays the lookup cost at most once. A sensor with no +// diagnosesActuator relationship, with multiple candidates, or whose lookup +// fails for any reason is marked unresolvable and skipped in future polls — +// emitting a maintenance order against a misidentified asset is a worse +// failure mode than emitting nothing. +// +// The expected graph shape is: +// +// afo:hasName "" . +// afo:diagnosesActuator . +// arrowhead:functionalLocation "" . +// +// The third triple is already in the Skoghall store for modelled equipment; +// the first two must be added by INSERT when the sensor's knowledge graph +// is published to GraphDB. +func (t *Traits) resolveActuators(sig *SignalT, nodes map[string][]components.NodeInfo) { + for node, nodeInfos := range nodes { + if _, ok := sig.ValveTagByNode[node]; ok { + continue + } + if sig.UnresolvableNodes[node] { + continue + } + if len(nodeInfos) == 0 { + continue + } + sensorName := assetNameFromURL(nodeInfos[0].URL) + if sensorName == "" { + log.Printf("nurse: cannot extract sensor name from %s; marking node %s unresolvable", + nodeInfos[0].URL, node) + sig.UnresolvableNodes[node] = true + continue + } + tag, err := resolveActuatorTag(t.GraphDB_URL, sensorName, 5*time.Second) + if err != nil { + log.Printf("nurse: cannot resolve actuator for sensor %s on node %s: %v", + sensorName, node, err) + sig.UnresolvableNodes[node] = true + continue + } + log.Printf("nurse: sensor %s on node %s diagnoses actuator at %s", + sensorName, node, tag) + sig.ValveTagByNode[node] = tag + } +} + +// resolveActuatorTag asks GraphDB for the FL tag of the actuator a sensor +// diagnoses. Returns the tag on success, or an error if the sensor has no +// diagnosesActuator triple, has more than one match (ambiguous), or the +// endpoint is unreachable. +func resolveActuatorTag(endpoint, sensorName string, timeout time.Duration) (string, error) { + query := fmt.Sprintf(`PREFIX afo: +PREFIX arrowhead: +SELECT ?valveTag WHERE { + ?sensor afo:hasName %q . + ?sensor afo:diagnosesActuator ?valveFL . + ?valveFL arrowhead:functionalLocation ?valveTag . +}`, sensorName) + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(query)) + if err != nil { + return "", fmt.Errorf("build request: %w", err) + } + req.Header.Set("Content-Type", "application/sparql-query") + req.Header.Set("Accept", "application/sparql-results+json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + body, _ := io.ReadAll(resp.Body) + return "", fmt.Errorf("HTTP %s: %s", resp.Status, strings.TrimSpace(string(body))) + } + + var result struct { + Results struct { + Bindings []map[string]struct { + Value string `json:"value"` + } `json:"bindings"` + } `json:"results"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return "", fmt.Errorf("decode response: %w", err) + } + + bs := result.Results.Bindings + switch len(bs) { + case 0: + return "", fmt.Errorf("no diagnosesActuator relationship for sensor %q", sensorName) + case 1: + tag := bs[0]["valveTag"].Value + if tag == "" { + return "", fmt.Errorf("empty valveTag binding for sensor %q", sensorName) + } + return tag, nil + default: + return "", fmt.Errorf("ambiguous diagnosesActuator for sensor %q (%d matches)", + sensorName, len(bs)) + } +} + +// CheckGraphDBUp probes the GraphDB SPARQL endpoint with a trivial query. +// It verifies both reachability and that the repository accepts SPARQL — +// a simple TCP/HTTP check would not catch a wrong repository name. Returns +// nil on success, an error explaining the failure otherwise. +func CheckGraphDBUp(endpoint string, timeout time.Duration) error { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + const probe = `SELECT (1 AS ?ok) WHERE {}` + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(probe)) + if err != nil { + return fmt.Errorf("build request: %w", err) + } + req.Header.Set("Content-Type", "application/sparql-query") + req.Header.Set("Accept", "application/sparql-results+json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("status %s: %s", resp.Status, strings.TrimSpace(string(body))) + } + return nil +} + // assetNameFromURL extracts the unit asset name from an Arrowhead service URL. // The path structure is ///, so the asset is segment [2]. func assetNameFromURL(rawURL string) string { @@ -449,7 +615,7 @@ func (t *Traits) requestMaintenanceOrder(sig *SignalT, equipmentID string, locat EquipmentID: equipmentID, FunctionalLocation: location, Plant: "1000", - Description: fmt.Sprintf("Signal %s exceeded threshold %.2f", sig.Name, sig.Threshold), + Description: fmt.Sprintf("Signal %s out of range [%.2f, %.2f]", sig.Name, sig.LowerThreshold, sig.UpperThreshold), Priority: "3", MaintenanceOrderType: "PM01", PlannedStartTime: &start, diff --git a/nurse/thing_test.go b/nurse/thing_test.go index c006915..8f624f7 100644 --- a/nurse/thing_test.go +++ b/nurse/thing_test.go @@ -56,16 +56,16 @@ func TestInitTemplate(t *testing.T) { func TestFindSignal_Found(t *testing.T) { tr := &Traits{ Signals: []SignalT{ - {Name: "temperature", Threshold: 75.0}, - {Name: "pressure", Threshold: 10.0}, + {Name: "temperature", LowerThreshold: 0.0, UpperThreshold: 75.0}, + {Name: "pressure", LowerThreshold: 10.0, UpperThreshold: 25.0}, }, } s := tr.findSignal("pressure") if s == nil { t.Fatal("expected to find 'pressure' signal, got nil") } - if s.Threshold != 10.0 { - t.Errorf("threshold = %v, want 10.0", s.Threshold) + if s.LowerThreshold != 10.0 || s.UpperThreshold != 25.0 { + t.Errorf("range = [%v, %v], want [10.0, 25.0]", s.LowerThreshold, s.UpperThreshold) } } @@ -227,11 +227,12 @@ func newTraitsWithPendingOrder(orderID, signalName string) *Traits { SAP_URL: "http://localhost", Signals: []SignalT{ { - Name: signalName, - Threshold: 75.0, - Operational: false, - TOverCount: map[string]int{"node1": 5}, - WorkRequested: map[string]bool{"node1": true}, + Name: signalName, + LowerThreshold: 10.0, + UpperThreshold: 25.0, + Operational: false, + TOverCount: map[string]int{"node1": 5}, + WorkRequested: map[string]bool{"node1": true}, }, }, pendingOrders: map[string]string{orderID: signalName}, @@ -323,7 +324,7 @@ func TestState(t *testing.T) { tr := &Traits{ SAP_URL: "http://localhost", Signals: []SignalT{ - {Name: "temperature", Threshold: 75.0, Operational: true, TOverCount: map[string]int{}, WorkRequested: map[string]bool{}}, + {Name: "temperature", LowerThreshold: 0.0, UpperThreshold: 75.0, Operational: true, TOverCount: map[string]int{}, WorkRequested: map[string]bool{}}, }, } tr.ua = &components.UnitAsset{Name: "HealthTracker"} @@ -336,7 +337,7 @@ func TestState(t *testing.T) { t.Errorf("state output missing signal name 'temperature': %s", body) } if !strings.Contains(body, "75") { - t.Errorf("state output missing threshold value: %s", body) + t.Errorf("state output missing upper threshold value: %s", body) } } @@ -345,7 +346,7 @@ func TestState(t *testing.T) { func TestServing_GET_monitor(t *testing.T) { tr := &Traits{ Signals: []SignalT{ - {Name: "temperature", Threshold: 80.0, Operational: true, TOverCount: map[string]int{}, WorkRequested: map[string]bool{}}, + {Name: "temperature", LowerThreshold: 0.0, UpperThreshold: 80.0, Operational: true, TOverCount: map[string]int{}, WorkRequested: map[string]bool{}}, }, pendingOrders: map[string]string{}, } diff --git a/orchestrator/go.mod b/orchestrator/go.mod index 2143ba5..01f4936 100644 --- a/orchestrator/go.mod +++ b/orchestrator/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/orchestrator -go 1.26.2 +go 1.26.3 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/parallax/go.mod b/parallax/go.mod index 4f7ed0b..04adc72 100644 --- a/parallax/go.mod +++ b/parallax/go.mod @@ -1,6 +1,6 @@ module github.com/sdoque/systems/parallax -go 1.26.2 +go 1.26.3 require ( github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/photographer/go.mod b/photographer/go.mod index 6641d4a..12b2522 100644 --- a/photographer/go.mod +++ b/photographer/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/photographer -go 1.26.2 +go 1.26.3 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/recognizer/go.mod b/recognizer/go.mod index 1feb3d2..5932250 100644 --- a/recognizer/go.mod +++ b/recognizer/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/recognizer -go 1.26.2 +go 1.26.3 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/revolutionary/go.mod b/revolutionary/go.mod index 496d967..d607166 100644 --- a/revolutionary/go.mod +++ b/revolutionary/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/revolutionary -go 1.26.2 +go 1.26.3 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/sailor/go.mod b/sailor/go.mod index bb48aa0..75b7df2 100644 --- a/sailor/go.mod +++ b/sailor/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/sailor -go 1.26.2 +go 1.26.3 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/sapper/go.mod b/sapper/go.mod index f4f38fe..a878ca8 100644 --- a/sapper/go.mod +++ b/sapper/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/sapper -go 1.26.2 +go 1.26.3 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/telegrapher/go.mod b/telegrapher/go.mod index badb476..1cd306b 100644 --- a/telegrapher/go.mod +++ b/telegrapher/go.mod @@ -1,6 +1,6 @@ module github.com/sdoque/systems/telegrapher -go 1.26.2 +go 1.26.3 require ( github.com/eclipse/paho.mqtt.golang v1.5.1 diff --git a/thermostat/go.mod b/thermostat/go.mod index 04c2504..53edc79 100644 --- a/thermostat/go.mod +++ b/thermostat/go.mod @@ -1,5 +1,5 @@ module github.com/sdoque/systems/thermostat -go 1.26.2 +go 1.26.3 require github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/tracker/go.mod b/tracker/go.mod index 2db78e4..015cb8f 100644 --- a/tracker/go.mod +++ b/tracker/go.mod @@ -1,6 +1,6 @@ module github.com/sdoque/systems/tracker -go 1.26.2 +go 1.26.3 require ( github.com/sdoque/mbaigo v0.1.0-alpha.7 diff --git a/uaclient/go.mod b/uaclient/go.mod index 562da04..bf28c80 100644 --- a/uaclient/go.mod +++ b/uaclient/go.mod @@ -1,6 +1,6 @@ module github.com/sdoque/systems/uaclient -go 1.26.2 +go 1.26.3 require ( github.com/gopcua/opcua v0.6.5 diff --git a/weatherman/go.mod b/weatherman/go.mod index 39afbab..5f9759b 100644 --- a/weatherman/go.mod +++ b/weatherman/go.mod @@ -1,6 +1,6 @@ module github.com/sdoque/systems/weatherman -go 1.26.2 +go 1.26.3 require ( github.com/sdoque/mbaigo v0.1.0-alpha.7