From 06acbdc83d5e33f1a15e9be21446147f77f33e3c Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Wed, 5 Jun 2024 12:06:07 +0100 Subject: [PATCH 01/36] Draft a spec for the Edge Agent split protocol --- acs-edge/docs/edge-split.md | 230 ++++++++++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 acs-edge/docs/edge-split.md diff --git a/acs-edge/docs/edge-split.md b/acs-edge/docs/edge-split.md new file mode 100644 index 00000000..3a06c4f8 --- /dev/null +++ b/acs-edge/docs/edge-split.md @@ -0,0 +1,230 @@ +# Edge Agent driver protocol + +This is a specification for a proposed protocol between the two halves +of a divided Edge Agent. The top half (the Edge Agent) will handle +configuration, Sparkplug encoding, report-by-exception, and interaction +with the rest of Factory+. The bottom half (the driver) will handle +getting data out of the southbound device and making it available to the +Edge Agent. + +## Definitions + +These are some terms used below. + +### Address + +This represents a particular data source within a driver's southbound +device. The address itself is a string in a driver-specific format. The +driver can read an address, the result of which is a binary data packet. + +### Address configuration + +A configuration packet passed from the Edge Agent to the driver +specifying the addresses currently in use and their data topic names. +This MUST take the form of a JSON object mapping from data topic name to +address. + +### Asynchronous (driver address) + +Some driver addresses are asynchronous, meaning that data arrives from +the data source without prompting from the driver. Some are only +asynchronous, meaning that the driver cannot request data and must wait +for it. Drivers are not expected to cache data or poll data sources on a +timer. + +### Connection name + +This is a name used by the Edge Agent to identify a particular driver. +This MUST be a short string consisting of letters, numbers and +underscores. The name is assigned by the Edge Agent configuration and +needs to be supplied to the driver when it is deployed. + +### Data packet + +When a driver reads an address, or an address pushes data to the driver +asynchronously, the result is a binary data packet. This data packet may +then be published on an appropriate data topic. Depending on its +configuration, the Edge Agent may expect this data packet to have a +certain format and may attempt to parse multiple Sparkplug metrics out +of a single packet. + +### Data topic name + +The Edge Agent configuration specifies the list of device addresses that +the Edge Agent is interested in at the moment. Because device addresses +are potentially long strings containing arbitrary characters, the Edge +Agent assigns a data topic name to each address it is currently using. +These MUST be short strings of numbers, letters and underscores. + +The Edge Agent MUST manage data topic names in such a way as to avoid +problems with synchronisation, and MUST NOT assume a driver will react +instantly to a new address configuration. + +### Driver configuration + +Configuration specific to a particular driver, containing connection +information and credentials and so on. This is in JSON form and is +passed to the Edge Agent as part of the Edge Agent configuration. + +### Edge Agent configuration + +The Edge Agent configuration is a JSON document managed by the Factory+ +infrastructure and retrieved by the Edge Agent. It contains information +about configured drivers and instructions for mapping driver data +packets to Sparkplug metrics. + +## Connection and authentication + +The Edge Agent provides an MQTT broker interface for drivers to +communicate with. Currently (due to likely implementation restrictions) +this will be an MQTT 3.1.1 broker supporting username/password +authentication only. Where a driver is run outside of the edge cluster +the Edge Agent broker port will need to be made available externally. + +The driver is an MQTT client. The driver MUST authenticate to the Edge +Agent broker using its connection name as both username and client-id, +and a password. This information must be supplied to the driver as part +of its deployment. + +Drivers which run as standalone applications SHOULD accept the following +environment variables: + +Name|Meaning +---|--- +`EDGE_MQTT`|URL of the Edge Agent MQTT broker +`EDGE_USERNAME`|Driver connection name +`EDGE_PASSWORD`|MQTT password + +Drivers MUST accept and support `mqtt://` URLs, including understanding +that the port defaults to 1883, and MAY accept `mqtts://`, `ws://` and +`wss://` URLs. + +## MQTT data packets + +Under normal circumstances MQTT QoS 0 should be adequate and avoids +additional overhead. However, to allow for situations where it is not, +drivers SHOULD obey these conventions: + +* The driver SHOULD set up its subscriptions and wait for the SUBACKs + before publishing anything. + +* The driver SHOULD subscribe with QoS 2 to allow the Edge Agent the + choice of what QoS to use. + +* The Edge Agent MAY downgrade the subscription in the SUBACK, and MAY + publish with a lower QoS. + +* The driver SHOULD observe the QoS granted for the `conf` topic and use + this QoS for all its PUBLISH packets. The driver SHOULD also observe + the QoS on all packets received on that topic and update its current + PUBLISH QoS. + +Drivers MAY instead choose to publish and subscribe entirely at QoS 0. + +Packets MUST NOT be published with the RETAIN flag set. The broker MUST +NOT allow retained messages. + +## MQTT topics + +All topics are under the namespace `fpEdge1`. Each driver then +communicates using topics under its own connection name. The Edge Agent +SHOULD disallow publish or subscribe packets to other topics. + +In these examples the connection name `Conn` will be used. + + fpEdge1/Conn/status + +The driver publishes to this topic. All messages MUST be a single string +from this table reporting on the status of the driver's southbound +connection: + +Status|Meaning +---|--- +`DOWN`|The driver is not connected or not running +`READY`|The driver is waiting for configuration +`UP`|The connection is up and ready +`CONF`|There is a problem with the driver configuration +`CONN`|The driver cannot connect because of networking problems +`AUTH`|The driver cannot authenticate southbound +`ERR`|Some other error has occurred + +`CONN` should be used for situations such as a hostname that won't +resolve or a southbound device not accepting incoming connections. +`AUTH` should be used if the driver has successfully connected to the +southbound device but cannot authenticate using the information in its +configuration. `ERR` should be used for other error situations, such as +a protocol error on the southbound connection. + +The driver MUST publish `READY` to this topic as soon as it has +connected to the broker and set up its subscriptions, unless it has +resumed an MQTT session and has its configuration already available. The +driver MUST include a LWT in its CONNECT packet publishing `DOWN` to +this topic. + +The driver MUST NOT publish data packets when the most recent message +published to this topic is anything other than `UP`. + + fpEdge1/Conn/conf + +The driver subscribes to this topic. The Edge Agent MUST publish the +driver configuration to this topic whenever the driver publishes a +`READY` status. The Edge Agent MAY publish a new configuration at any +time. + + fpEdge1/Conn/addr + +The driver subscribes to this topic. The Edge Agent publishes the +address configuration. The driver MUST record this information, and may +use it to configure its southbound connection. The Edge Agent MUST +republish the address configuration every time it publishes a driver +configuration. The Edge Agent MAY publish a new address configuration at +any time. + + fpEdge1/Conn/data/Data + +This represents a family of topics, where `Data` is a data topic name +sent by the Edge Agent. The driver MUST publish to these topics whenever +it has a new data packet required by the current address configuration. +The driver is not expected to avoid sending duplicate data packets. + +The driver MAY publish to these topics asynchronously if its southbound +data source is asynchronous. The driver SHOULD NOT attempt to poll on a +timer, leaving that up to the Edge Agent. + + fpEdge1/Conn/err/Data + +This represents a family of topics, where `Data` is a data topic name. +The driver publishes to these topics to report an error with a +particular address. + +If the driver has a problem reading an address included in the current +address configuration, it MUST publish a single string from the table +below to the appropriate error topic. If the driver later succeeds in +reading from the address it MUST clear the error by publishing an empty +message to the error topic. The driver MUST NOT publish to a data topic +when an error has been reported. + +Every time the driver publishes `UP` to the status topic this clears all +reported address errors. If the driver has a problem reading an address +which is likely to indicate a problem communicating with the southbound +device altogether, the driver SHOULD report this via the driver status +topic rather than an individual error topic. + +Error code|Meaning +---|--- +`CONN`|Connection problem +`AUTH`|Authentication or authorisation problem +`ERR`|Some other error condition + + fpEdge1/Conn/poll + +The driver subscribes to this topic. The Edge Agent publishes a message +to this topic to request the driver to poll certain addresses. This +message consists of previously-configured data topic names separated by +single newline characters. + +The driver MUST attempt to read the address associated with each data +topic listed and publish a data packet, as soon as possible. If an error +occurs reading the address, an address error or driver status error MUST +be published. If a particular address cannot be explicitly read because +the data source is entirely asynchronous it MUST be ignored. From 5a8cd9c8b802ab3d6c6406c9b80ddf9940152ed7 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Wed, 5 Jun 2024 12:51:54 +0100 Subject: [PATCH 02/36] Support CMDs We don't have a lot of experience using CMD all the way to a device in Factory+. In general supporting device CMDs is only possible when an address can be mapped straight to a single metric, with no 'path' component. --- acs-edge/docs/edge-split.md | 48 +++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/acs-edge/docs/edge-split.md b/acs-edge/docs/edge-split.md index 3a06c4f8..75e2e674 100644 --- a/acs-edge/docs/edge-split.md +++ b/acs-edge/docs/edge-split.md @@ -130,7 +130,8 @@ All topics are under the namespace `fpEdge1`. Each driver then communicates using topics under its own connection name. The Edge Agent SHOULD disallow publish or subscribe packets to other topics. -In these examples the connection name `Conn` will be used. +In these examples the connection name `Conn` will be used. Where +necessary the data topic name `Data` will also be used. fpEdge1/Conn/status @@ -182,40 +183,57 @@ any time. fpEdge1/Conn/data/Data -This represents a family of topics, where `Data` is a data topic name -sent by the Edge Agent. The driver MUST publish to these topics whenever -it has a new data packet required by the current address configuration. -The driver is not expected to avoid sending duplicate data packets. +The driver MUST publish to this topic whenever it has a new data packet +required by the current address configuration. The driver is not +expected to avoid sending duplicate data packets. -The driver MAY publish to these topics asynchronously if its southbound +The driver MAY publish to this topic asynchronously if its southbound data source is asynchronous. The driver SHOULD NOT attempt to poll on a timer, leaving that up to the Edge Agent. + fpEdge1/Conn/cmd/Data + +The driver subscribes to these topics, preferably with a wildcard +subscription. The Edge Agent publishes to these topics to write data to +the driver's southbound device. + +When the Edge Agent publishes to this topic, the driver SHOULD attempt +to write the data to its southbound device at the location given by the +corresponding address. If the driver is unable or unwilling to write to +the given address it should report an `RO` address error. Drivers MAY +refuse to write to southbound devices in general. + fpEdge1/Conn/err/Data -This represents a family of topics, where `Data` is a data topic name. -The driver publishes to these topics to report an error with a -particular address. +The driver publishes to this topic to report an error with a particular +address. -If the driver has a problem reading an address included in the current +If the driver has a problem accessing an address included in the current address configuration, it MUST publish a single string from the table below to the appropriate error topic. If the driver later succeeds in -reading from the address it MUST clear the error by publishing an empty +accessing the address it MUST clear the error by publishing an empty message to the error topic. The driver MUST NOT publish to a data topic when an error has been reported. Every time the driver publishes `UP` to the status topic this clears all -reported address errors. If the driver has a problem reading an address -which is likely to indicate a problem communicating with the southbound -device altogether, the driver SHOULD report this via the driver status -topic rather than an individual error topic. +reported address errors. If the driver has a problem accessing an +address which is likely to indicate a problem communicating with the +southbound device altogether, the driver SHOULD report this via the +driver status topic rather than an individual error topic. Error code|Meaning ---|--- `CONN`|Connection problem `AUTH`|Authentication or authorisation problem +`RO`|The Edge Agent has attempted to `cmd` a readonly address +`WO`|The Edge Agent has attempted to `poll` a writeonly address +`CMD`|The Edge Agent has sent a `cmd` which is not acceptable `ERR`|Some other error condition +The `CMD` code is for situations where the Edge Agent has published to a +`cmd` topic, and the driver believes it can write to the address, but +the format of the data packet supplied is not suitable for some reason. + fpEdge1/Conn/poll The driver subscribes to this topic. The Edge Agent publishes a message From 233366dfa9e07c8e6341b1bf325121e15fef2f51 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Wed, 19 Jun 2024 15:10:24 +0100 Subject: [PATCH 03/36] Add address groups and driver polling OPCUA and MTConnect support automatic polling within the protocol. S7 supports group queries. It is a good idea to allow drivers to be efficient where they can be. Also support an overall `active` topic for a driver, indicating whether the relevant Edge Agent connection is online. This also serves as a prompt for the driver to republish its status when the Edge Agent starts up. --- acs-edge/docs/edge-split.md | 51 ++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/acs-edge/docs/edge-split.md b/acs-edge/docs/edge-split.md index 75e2e674..288585e2 100644 --- a/acs-edge/docs/edge-split.md +++ b/acs-edge/docs/edge-split.md @@ -17,12 +17,31 @@ This represents a particular data source within a driver's southbound device. The address itself is a string in a driver-specific format. The driver can read an address, the result of which is a binary data packet. +### Address group + +Addresses may be collected into groups and each group is assigned a name +by the Edge Agent. Each group may be assigned a poll interval, which +indicates the frequency at which the Edge Agent would like to receive +updates to the addresses in the group. A single address MAY appear in +more than one group. + ### Address configuration A configuration packet passed from the Edge Agent to the driver -specifying the addresses currently in use and their data topic names. -This MUST take the form of a JSON object mapping from data topic name to -address. +specifying the addresses currently in use and their data topic names. A +given address MUST NOT be associated with more than one data topic name +within a given address configuration. The address configuration is in +JSON form and is an object with these properties: + +* `version`: The integer `1`. +* `addrs`: An object whose keys are data topic names and whose values + are addresses. +* `groups`: An object whose keys are group names and whose values are + objects with these properties: + * `addrs`: An array of data topic names from the top-level `addrs` + property. + * `poll`: An integer giving the requested poll interval in + milliseconds for this group. ### Asynchronous (driver address) @@ -165,6 +184,16 @@ this topic. The driver MUST NOT publish data packets when the most recent message published to this topic is anything other than `UP`. + fpEdge1/Conn/active + +The driver subscribes to this topic. All messages MUST be a single +string, either `ONLINE` or `OFFLINE`, indicating whether the Edge Agent +expects this driver to be active. On receipt of an `ONLINE` status, the +driver MUST publish its current status to its `status` topic. On receipt +of an `OFFLINE` status, the driver MUST discard any configuration and +revert to `READY` status, and SHOULD disconnect from its data sources if +possible. + fpEdge1/Conn/conf The driver subscribes to this topic. The Edge Agent MUST publish the @@ -181,6 +210,22 @@ republish the address configuration every time it publishes a driver configuration. The Edge Agent MAY publish a new address configuration at any time. +A driver receiving a `version` it does not recognise MUST set its status +to `CONF`. A driver MUST accept and ignore additional keys it does not +recognise. The Edge Agent MUST NOT supply additional keys except in +compliance with an updated version of this specification. + +The driver SHOULD only attempt to honour a requested poll interval if +its device connection provides facilities for polling a set of +addresses. If data is not provided by the driver on time the Edge Agent +MUST poll explicitly. + +If a driver is performing polling it SHOULD attempt to provide new +values for all addresses in a group in quick succession. The Edge Agent +MUST assume that any reported value stands until a new value is +reported and MAY choose to batch values reported upstream over +Sparkplug. + fpEdge1/Conn/data/Data The driver MUST publish to this topic whenever it has a new data packet From 668e52fa2c626a618168175ab66b9426c5ada042 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Tue, 18 Jun 2024 12:38:28 +0100 Subject: [PATCH 04/36] Remove `noImplicitAny` This makes TypeScript a bit more bearable. --- acs-edge/lib/devices/MQTT.ts | 2 +- acs-edge/lib/devices/websocket.ts | 2 +- acs-edge/tsconfig.json | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/acs-edge/lib/devices/MQTT.ts b/acs-edge/lib/devices/MQTT.ts index 0ba1ca62..8d60fa12 100644 --- a/acs-edge/lib/devices/MQTT.ts +++ b/acs-edge/lib/devices/MQTT.ts @@ -107,7 +107,7 @@ export class MQTTConnection extends DeviceConnection { * @param delimiter */ writeMetrics(metrics: Metrics, writeCallback: Function, payloadFormat?: string, delimiter?: string) { - let err = null; + let err: Error|null = null; metrics.addresses.forEach((addr) => { let payload = writeValuesToPayload(metrics.getByAddress(addr), payloadFormat || ""); if (payload && payload.length) { diff --git a/acs-edge/lib/devices/websocket.ts b/acs-edge/lib/devices/websocket.ts index 7e20d8d9..d4d82b33 100644 --- a/acs-edge/lib/devices/websocket.ts +++ b/acs-edge/lib/devices/websocket.ts @@ -47,7 +47,7 @@ export class WebsocketConnection extends DeviceConnection { * @param {Array} metrics Array of metric objects to write to device connection */ writeMetrics(metrics: Metrics, writeCallback: Function, payloadFormat?: string, delimiter?: string) { - let err = null; + let err: Error|null = null; let payload = writeValuesToPayload(metrics.array, payloadFormat || ""); if (payload && payload.length) { this.#ws.send(payload, (err) => { diff --git a/acs-edge/tsconfig.json b/acs-edge/tsconfig.json index 719bdfb3..a1472c72 100644 --- a/acs-edge/tsconfig.json +++ b/acs-edge/tsconfig.json @@ -3,7 +3,7 @@ "target": "ES6", "allowJs": true, "moduleResolution": "Node", - "noImplicitAny": true, + "noImplicitAny": false, "removeComments": true, "preserveConstEnums": true, "sourceMap": false, @@ -17,6 +17,7 @@ "**/__mocks__/*", "../../node_modules", "build", + "split", "tests" ] } From f8e14dfe0b51d039b07665e71808764ea15d31c5 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Tue, 18 Jun 2024 12:39:56 +0100 Subject: [PATCH 05/36] Run a broker for drivers to connect to We are using the Aedes broker library. Create a broker and perform auth on clients. The broker URL to listen on and the location of the passwords for the drivers are taken from the environment rather than from the config as these will need to be synchronised with the driver deployment. This means they are Helm values rather than edge agent config items. --- acs-edge/Makefile | 6 + acs-edge/app.ts | 14 +- acs-edge/lib/driverBroker.ts | 130 ++++++++++++++++++ acs-edge/lib/translator.ts | 12 +- acs-edge/package-lock.json | 259 +++++++++++++++++++++++++++++++++++ acs-edge/package.json | 1 + 6 files changed, 416 insertions(+), 6 deletions(-) create mode 100644 acs-edge/lib/driverBroker.ts diff --git a/acs-edge/Makefile b/acs-edge/Makefile index 9d3cc657..a1e06c11 100644 --- a/acs-edge/Makefile +++ b/acs-edge/Makefile @@ -5,3 +5,9 @@ repo?=acs-edge # Don't set k8s.deployment, the deployment doesn't have a fixed name. include ${mk}/acs.js.mk + +local.build: + npx tsc --project tsconfig.json + +local.run: local.build + node build/app.js diff --git a/acs-edge/app.ts b/acs-edge/app.ts index 3dfe50d4..abbf1e03 100644 --- a/acs-edge/app.ts +++ b/acs-edge/app.ts @@ -3,13 +3,16 @@ * Copyright 2023 AMRC */ -import {ServiceClient, UUIDs} from "@amrc-factoryplus/utilities"; -import {Translator} from "./lib/translator.js"; -import {log} from "./lib/helpers/log.js"; -import {GIT_VERSION} from "./lib/git-version.js"; import * as dotenv from 'dotenv'; import sourceMapSupport from 'source-map-support' +import {ServiceClient, UUIDs} from "@amrc-factoryplus/utilities"; + +import {DriverBroker} from "./lib/driverBroker.js"; +import {GIT_VERSION} from "./lib/git-version.js"; +import {log} from "./lib/helpers/log.js"; +import {Translator} from "./lib/translator.js"; + sourceMapSupport.install() dotenv.config({path: '../.env'}); @@ -20,9 +23,10 @@ async function run() { const pollInt = parseInt(process.env.POLL_INT) || 30; const fplus = await new ServiceClient({ env: process.env }).init(); + const broker = new DriverBroker(process.env); // Once a configuration has been loaded then start up the translator - let transApp = new Translator(fplus, pollInt); + let transApp = new Translator(fplus, pollInt, broker); process.once('SIGTERM', () => { log('🔪️SIGTERM RECEIVED'); transApp.stop(true); diff --git a/acs-edge/lib/driverBroker.ts b/acs-edge/lib/driverBroker.ts new file mode 100644 index 00000000..b65fd449 --- /dev/null +++ b/acs-edge/lib/driverBroker.ts @@ -0,0 +1,130 @@ +/* + * Factory+ / AMRC Connectivity Stack (ACS) Edge component + * Copyright 2024 AMRC + */ + +import { EventEmitter } from "events"; +import fs from "fs/promises"; +import net from "net"; +import util from "util"; + +import Aedes from "aedes"; + +const prefix = "fpEdge1"; +const topicrx = new RegExp(`^${prefix}/(\\w+)/(.*)`); + +function log (f, ...a) { + const msg = util.format(f, ...a); + console.log("DRIVER: %s", msg); +} + +export class DriverBroker extends EventEmitter { + broker: Aedes + passwords: string + publish: Map + subscribe: Map + hostname: string + port: number + + constructor (env) { + super(); + + const url = new URL(env.EDGE_MQTT); + + if (url.protocol != "mqtt:") + throw new Error(`Unknown URL scheme ${url.protocol}`); + + this.hostname = url.hostname; + this.port = url.port + ? Number.parseInt(url.port, 10) + : 1883; + + this.passwords = env.EDGE_PASSWORDS; + + this.broker = new Aedes(); + this.publish = new Map(); + this.subscribe = new Map(); + + const br = this.broker; + br.authenticate = this.auth.bind(this); + br.authorizePublish = this.authPub.bind(this); + br.authorizeSubscribe = this.authSub.bind(this); + + br.subscribe(`${prefix}/#`, (packet, callback) => { + callback(); + this.message(packet.topic, packet.payload); + }, () => {}); + } + + start () { + const srv = net.createServer(this.broker.handle); + srv.on("listening", () => + log("Listening: %o", srv.address())); + srv.listen(this.port, this.hostname); + } + + stop () { + return new Promise(resolve => + this.broker.close(resolve)); + } + + async auth (client, username, password, callback) { + const { id } = client; + log("AUTH: %s, %s, %s", id, username, password); + + const fail = (f, ...a) => { log(f, ...a); callback(null, false); }; + + if (id != username) + return fail("Invalid client-id %s for %s", id, username); + if (!password) + return fail("No password for %s", username); + const expect = await fs.readFile(`${this.passwords}/${username}`) + .catch(e => null); + if (!expect) + return fail("Unexpected driver %s", username); + if (expect.compare(password) != 0) + return fail("Bad password for %s", username); + + this.publish.set(id, new RegExp( + `^${prefix}/${id}/(?:status|data/\\w+|err/\\w+)$`)); + this.subscribe.set(id, new RegExp( + `^${prefix}/${id}/(?:conf|addr|cmd/\\w+|poll)$`)); + + callback(null, true); + } + + authPub (client, packet, callback) { + const { id } = client; + const { topic } = packet; + + log("PUBLISH: %s %s", id, topic); + if (packet.retain) + return callback(new Error("Retained PUBLISH forbidden")); + if (!this.publish.get(id)!.test(topic)) + return callback(new Error("Unauthorised PUBLISH")); + callback(null); + } + + authSub (client, subscription, callback) { + const { id } = client; + const { topic } = subscription; + + log("SUBSCRIBE: %s %s", id, topic); + if (!this.subscribe.get(id)!.test(topic)) + return callback(new Error("Unauthorised SUBSCRIBE")); + callback(null, subscription); + } + + message (topic, payload) { + log("PACKET: %s %o", topic, payload); + + const match = topic.match(topicrx); + if (!match) { + log("Received message on unknown topic %s", topic); + return; + } + + const [, id, path] = match; + this.emit("message", id, path, payload); + } +} diff --git a/acs-edge/lib/translator.ts b/acs-edge/lib/translator.ts index 6eaaac1d..c0a50170 100644 --- a/acs-edge/lib/translator.ts +++ b/acs-edge/lib/translator.ts @@ -15,6 +15,7 @@ import {reHashConf} from "../utils/FormatConfig.js"; // Import device connections import {SparkplugNode} from "./sparkplugNode.js"; +import {DriverBroker} from "./driverBroker.js"; // Import devices import {RestConnection, RestDevice} from "./devices/REST.js"; @@ -55,6 +56,7 @@ export class Translator extends EventEmitter { */ sparkplugNode!: SparkplugNode fplus: ServiceClient + broker: DriverBroker pollInt: number connections: { @@ -64,10 +66,11 @@ export class Translator extends EventEmitter { [index: string]: any } - constructor(fplus: ServiceClient, pollInt: number) { + constructor(fplus: ServiceClient, pollInt: number, broker: DriverBroker) { super(); this.fplus = fplus; + this.broker = broker; this.pollInt = pollInt; this.connections = {}; this.devices = {}; @@ -95,6 +98,11 @@ export class Translator extends EventEmitter { // Setup Sparkplug node handlers this.setupSparkplug(); + + log("Starting driver broker..."); + this.broker.on("message", (d, t, p) => + log(`Driver message: ${d} ${t}`)); + this.broker.start(); } catch (e: any) { log(`Error starting translator: ${e.message}`); console.error((e as Error).stack); @@ -119,6 +127,8 @@ export class Translator extends EventEmitter { log(`Closing connection ${connection._type}`); connection.close(); })); + log("Stopping driver broker..."); + await this.broker.stop(); log('Waiting for sparkplug node to stop...'); await this.sparkplugNode?.stop(); diff --git a/acs-edge/package-lock.json b/acs-edge/package-lock.json index 9bc8064c..537cf161 100644 --- a/acs-edge/package-lock.json +++ b/acs-edge/package-lock.json @@ -17,6 +17,7 @@ "@types/ws": "^8.2.0", "@types/xmldom": "^0.1.31", "@xmldom/xmldom": "^0.8.6", + "aedes": "^0.51.2", "ajv-formats": "^2.1.1", "axios": "^0.21.4", "cookie-parser": "^1.4.5", @@ -1737,6 +1738,140 @@ "node": ">=0.4.0" } }, + "node_modules/aedes": { + "version": "0.51.2", + "resolved": "https://registry.npmjs.org/aedes/-/aedes-0.51.2.tgz", + "integrity": "sha512-G4jYcv7vocEsDC860SPYxpeBABhCZp4na5kwJaGMcrs2f8TnjOsKNf3r0QrkGmmzEPARhLsRb1XshZqdchc20w==", + "dependencies": { + "aedes-packet": "^3.0.0", + "aedes-persistence": "^9.1.2", + "end-of-stream": "^1.4.4", + "fastfall": "^1.5.1", + "fastparallel": "^2.4.1", + "fastseries": "^2.0.0", + "hyperid": "^3.2.0", + "mqemitter": "^6.0.0", + "mqtt-packet": "^9.0.0", + "retimer": "^4.0.0", + "reusify": "^1.0.4", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/aedes" + } + }, + "node_modules/aedes-packet": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aedes-packet/-/aedes-packet-3.0.0.tgz", + "integrity": "sha512-swASey0BxGs4/npZGWoiVDmnEyPvVFIRY6l2LVKL4rbiW8IhcIGDLfnb20Qo8U20itXlitAKPQ3MVTEbOGG5ZA==", + "dependencies": { + "mqtt-packet": "^7.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/aedes-packet/node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/aedes-packet/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/aedes-packet/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/aedes-packet/node_modules/mqtt-packet": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-7.1.2.tgz", + "integrity": "sha512-FFZbcZ2omsf4c5TxEQfcX9hI+JzDpDKPT46OmeIBpVA7+t32ey25UNqlqNXTmeZOr5BLsSIERpQQLsFWJS94SQ==", + "dependencies": { + "bl": "^4.0.2", + "debug": "^4.1.1", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/aedes-packet/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/aedes-packet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/aedes-packet/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/aedes-persistence": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/aedes-persistence/-/aedes-persistence-9.1.2.tgz", + "integrity": "sha512-2Wlr5pwIK0eQOkiTwb8ZF6C20s8UPUlnsJ4kXYePZ3JlQl0NbBA176mzM8wY294BJ5wybpNc9P5XEQxqadRNcQ==", + "dependencies": { + "aedes-packet": "^3.0.0", + "qlobber": "^7.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -3311,6 +3446,14 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -3569,6 +3712,31 @@ "node": ">=16.1.0" } }, + "node_modules/fastfall": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/fastfall/-/fastfall-1.5.1.tgz", + "integrity": "sha512-KH6p+Z8AKPXnmA7+Iz2Lh8ARCMr+8WNPVludm1LGkZoD2MjY6LVnRMtTKhkdzI+jr0RzQWXKzKyBJm1zoHEL4Q==", + "dependencies": { + "reusify": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fastparallel": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/fastparallel/-/fastparallel-2.4.1.tgz", + "integrity": "sha512-qUmhxPgNHmvRjZKBFUNI0oZuuH9OlSIOXmJ98lhKPxMZZ7zS/Fi0wRHOihDSz0R1YiIOjxzOY4bq65YTcdBi2Q==", + "dependencies": { + "reusify": "^1.0.4", + "xtend": "^4.0.2" + } + }, + "node_modules/fastseries": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fastseries/-/fastseries-2.0.0.tgz", + "integrity": "sha512-XBU9RXeoYc2/VnvMhplAxEmZLfIk7cvTBu+xwoBuTI8pL19E03cmca17QQycKIdxgwCeFA/a4u27gv1h3ya5LQ==" + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -4198,6 +4366,47 @@ "node": "*" } }, + "node_modules/hyperid": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/hyperid/-/hyperid-3.2.0.tgz", + "integrity": "sha512-PdTtDo+Rmza9nEhTunaDSUKwbC69TIzLEpZUwiB6f+0oqmY0UPfhyHCPt6K1NQ4WFv5yJBTG5vELztVWP+nEVQ==", + "dependencies": { + "buffer": "^5.2.1", + "uuid": "^8.3.2", + "uuid-parse": "^1.1.0" + } + }, + "node_modules/hyperid/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/hyperid/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -5806,6 +6015,26 @@ "node": ">=10" } }, + "node_modules/mqemitter": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/mqemitter/-/mqemitter-6.0.0.tgz", + "integrity": "sha512-6+dd6arKyuX7c622gLMOSviiAIhORGNCi7r4KTft/XDetuedNZwRZWsED99nDeHPhkaftj7u7SlAbGhpFt319w==", + "dependencies": { + "fastparallel": "^2.4.1", + "qlobber": "^8.0.1" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/mqemitter/node_modules/qlobber": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/qlobber/-/qlobber-8.0.1.tgz", + "integrity": "sha512-O+Wd1chXj5YE1DwmD+ae0bXiSLehmnS3czlC1R9FL/Nt/3q8uMS1bIHmg2lJfCoiimCxClWM8AAuJrF0EvNiog==", + "engines": { + "node": ">= 16" + } + }, "node_modules/mqtt": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.5.0.tgz", @@ -7973,6 +8202,14 @@ "node": ">=6.0.0" } }, + "node_modules/qlobber": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/qlobber/-/qlobber-7.0.1.tgz", + "integrity": "sha512-FsFg9lMuMEFNKmTO9nV7tlyPhx8BmskPPjH2akWycuYVTtWaVwhW5yCHLJQ6Q+3mvw5cFX2vMfW2l9z2SiYAbg==", + "engines": { + "node": ">= 14" + } + }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -8201,6 +8438,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/retimer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retimer/-/retimer-4.0.0.tgz", + "integrity": "sha512-fZIVtvbOsQsxNSDhpdPOX4lx5Ss2ni+S72AUBitARpFhtA3UzrAjQ6gDtypB2/+l7L+1VQgAgpvAKY66mElH0w==", + "dependencies": { + "worker-timers": "^7.0.75" + } + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -8209,6 +8454,15 @@ "node": ">= 4" } }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rfdc": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", @@ -9292,6 +9546,11 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/uuid-parse": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/uuid-parse/-/uuid-parse-1.1.0.tgz", + "integrity": "sha512-OdmXxA8rDsQ7YpNVbKSJkNzTw2I+S5WsbMDnCtIWSQaosNAcWtFuI/YK1TjzUI6nbkgiqEyh8gWngfcv8Asd9A==" + }, "node_modules/v8-to-istanbul": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", diff --git a/acs-edge/package.json b/acs-edge/package.json index 1ee14280..22dfdf9a 100644 --- a/acs-edge/package.json +++ b/acs-edge/package.json @@ -37,6 +37,7 @@ "@types/ws": "^8.2.0", "@types/xmldom": "^0.1.31", "@xmldom/xmldom": "^0.8.6", + "aedes": "^0.51.2", "ajv-formats": "^2.1.1", "axios": "^0.21.4", "cookie-parser": "^1.4.5", From df852b7549064733437c305a054e21773eb2ee11 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Wed, 19 Jun 2024 09:46:51 +0100 Subject: [PATCH 06/36] Emit structured driver messages The messages from a driver are too complicated to try to emit as positional arguments; emit an object instead. This means we can pull out and validate the data topic name too. --- acs-edge/lib/driverBroker.ts | 6 +++--- acs-edge/lib/translator.ts | 23 ++++++++++++----------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/acs-edge/lib/driverBroker.ts b/acs-edge/lib/driverBroker.ts index b65fd449..9e0c45e3 100644 --- a/acs-edge/lib/driverBroker.ts +++ b/acs-edge/lib/driverBroker.ts @@ -11,7 +11,7 @@ import util from "util"; import Aedes from "aedes"; const prefix = "fpEdge1"; -const topicrx = new RegExp(`^${prefix}/(\\w+)/(.*)`); +const topicrx = new RegExp(`^${prefix}/(\\w+)/(\\w+)(?:/(\\w+))?$`); function log (f, ...a) { const msg = util.format(f, ...a); @@ -124,7 +124,7 @@ export class DriverBroker extends EventEmitter { return; } - const [, id, path] = match; - this.emit("message", id, path, payload); + const [, id, msg, data] = match; + this.emit("message", { id, msg, data, payload }); } } diff --git a/acs-edge/lib/translator.ts b/acs-edge/lib/translator.ts index c0a50170..825d8613 100644 --- a/acs-edge/lib/translator.ts +++ b/acs-edge/lib/translator.ts @@ -3,7 +3,11 @@ * Copyright 2023 AMRC */ +import {EventEmitter} from "events"; +import fs from "fs"; import timers from "timers/promises"; +import util from "util"; + import type {Identity} from "@amrc-factoryplus/utilities"; import {ServiceClient} from "@amrc-factoryplus/utilities"; @@ -13,11 +17,14 @@ import {ServiceClient} from "@amrc-factoryplus/utilities"; import {validateConfig} from '../utils/CentralConfig.js'; import {reHashConf} from "../utils/FormatConfig.js"; -// Import device connections -import {SparkplugNode} from "./sparkplugNode.js"; +import {Device, deviceOptions} from "./device.js"; import {DriverBroker} from "./driverBroker.js"; +import {SparkplugNode} from "./sparkplugNode.js"; +import * as UUIDs from "./uuids.js"; + +import {log} from "./helpers/log.js"; +import {sparkplugConfig,} from "./helpers/typeHandler.js"; -// Import devices import {RestConnection, RestDevice} from "./devices/REST.js"; import {S7Connection, S7Device} from "./devices/S7.js"; import {OPCUAConnection, OPCUADevice} from "./devices/OPCUA.js"; @@ -26,12 +33,6 @@ import {UDPConnection, UDPDevice} from "./devices/UDP.js"; import {WebsocketConnection, WebsocketDevice} from "./devices/websocket.js"; import {MTConnectConnection, MTConnectDevice} from "./devices/MTConnect.js"; import {EtherNetIPConnection, EtherNetIPDevice} from "./devices/EtherNetIP.js"; -import {log} from "./helpers/log.js"; -import {sparkplugConfig,} from "./helpers/typeHandler.js"; -import {Device, deviceOptions} from "./device.js"; -import * as UUIDs from "./uuids.js"; -import {EventEmitter} from "events"; -import fs from "node:fs"; /** * Translator class basically turns config file into instantiated classes @@ -100,8 +101,8 @@ export class Translator extends EventEmitter { this.setupSparkplug(); log("Starting driver broker..."); - this.broker.on("message", (d, t, p) => - log(`Driver message: ${d} ${t}`)); + this.broker.on("message", msg => + log(util.format("Driver message: %O", msg))); this.broker.start(); } catch (e: any) { log(`Error starting translator: ${e.message}`); From 1c65a6bce73304d331e964c7abea9e845c130994 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Wed, 19 Jun 2024 09:48:41 +0100 Subject: [PATCH 07/36] Start a TODO list These tasks are too small to go in Jira, and some will probably outlive this project. --- acs-edge/docs/TODO.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 acs-edge/docs/TODO.md diff --git a/acs-edge/docs/TODO.md b/acs-edge/docs/TODO.md new file mode 100644 index 00000000..0bfdab0a --- /dev/null +++ b/acs-edge/docs/TODO.md @@ -0,0 +1,33 @@ +# TODO list for edge-split work + +The Device subclasses need to go. Where they do work this needs to move +into the Connection. In particular some Devices handle subscription +tasks which should move to `startSubscription`. + +`DeviceConnection.readMetrics` accepts payload format / delimiter +arguments. I don't think any of the drivers use them? This belongs +EA-side. + +`writeMetrics` also accepts format/delimiter. I'm not clear yet that it +isn't used for this code path. Ideally we want all device writes to +accept a plain Buffer as might be provided from a read. + +The Connection currently handles the poll loop, as part of +`startSubscription`. This is implemented entirely in the base class and +should be handled by the Device (EA-side). + +More generally, the Connections shouldn't see the Metrics at all. They +should operator entirely on addresses. + +Multiple Devices may subscribe to a single Connection. The EA-side +Connection will need to track the current list of addresses we are +interested in and push it down to the driver. + +Devices are currently linked to a single Connection. This is not +necessary, but means we need to: +* Add a `connection` property to each metric definition. +* Change the Device to poll via a central connection manifold rather + than via an individual connection. +* Supply data topic names to a Device in return for addresses when it + subscribes to the connection manifold. +* Poll the manifold using connection/datatopic pairs. From 81f494259801943f4d409f4d1eabbcf07ad7734f Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Wed, 19 Jun 2024 11:41:01 +0100 Subject: [PATCH 08/36] Remove Device subclasses Device represents a Sparkplug Device, and clearly lives on the Edge Agent side of the divide. It should not have any connection-specific functionality. Where there was specific code, this was concerned with arranging to subscribe to addresses on the southbound connection. Handle this in the Connection class instead. --- acs-edge/lib/device.ts | 35 ++++++++++++-- acs-edge/lib/devices/EtherNetIP.ts | 22 --------- acs-edge/lib/devices/MQTT.ts | 55 +++++++--------------- acs-edge/lib/devices/MTConnect.ts | 34 +------------- acs-edge/lib/devices/OPCUA.ts | 33 +------------ acs-edge/lib/devices/REST.ts | 17 ------- acs-edge/lib/devices/S7.ts | 65 +++++++------------------- acs-edge/lib/devices/UDP.ts | 41 ---------------- acs-edge/lib/devices/templateDevice.ts | 34 +++++--------- acs-edge/lib/devices/websocket.ts | 32 ------------- acs-edge/lib/translator.ts | 39 ++++++++-------- 11 files changed, 100 insertions(+), 307 deletions(-) diff --git a/acs-edge/lib/device.ts b/acs-edge/lib/device.ts index 28f93526..31c1b8aa 100644 --- a/acs-edge/lib/device.ts +++ b/acs-edge/lib/device.ts @@ -51,6 +51,7 @@ export abstract class DeviceConnection extends EventEmitter { #intHandles: { [index: string]: ReturnType } + #subHandles: Map /** * Basic class constructor, doesn't do much. Must emit a 'ready' event when complete. @@ -63,7 +64,10 @@ export abstract class DeviceConnection extends EventEmitter { this._type = type; // Define object of polling interval handles for each device this.#intHandles = {}; + // Collection of subscription handles + this.#subHandles = new Map(); // Emit ready event + /* XXX this has no listeners */ this.emit('ready'); } @@ -100,6 +104,24 @@ export abstract class DeviceConnection extends EventEmitter { writeCallback(err); } + /** + * Perform any setup needed to read from certain addresses, e.g. set + * up MQTT subscriptions. This does not attempt to detect duplicate + * requests. + * @param addresses Addresses to start watching + */ + async subscribe (addresses: string[]): Promise { + return null; + } + + /** + * Undo any setup performed by `subscribe`. + * @param addresses Addresses to stop watching + */ + async unsubscribe (handle: any): Promise { + return; + } + /** * * @param metrics Metrics object to watch @@ -109,8 +131,9 @@ export abstract class DeviceConnection extends EventEmitter { * @param deviceId The device ID whose metrics are to be watched * @param subscriptionStartCallback A function to call once the subscription has been setup */ - startSubscription(metrics: Metrics, payloadFormat: serialisationType, delimiter: string, interval: number, deviceId: string, subscriptionStartCallback: Function) { - + async startSubscription(metrics: Metrics, payloadFormat: serialisationType, delimiter: string, interval: number, deviceId: string, subscriptionStartCallback: Function) { + this.#subHandles.set(deviceId, + await this.subscribe(metrics.addresses)); this.#intHandles[deviceId] = setInterval(() => { this.readMetrics(metrics, payloadFormat, delimiter); @@ -123,9 +146,11 @@ export abstract class DeviceConnection extends EventEmitter { * @param deviceId The device ID we are cancelling the subscription for * @param stopSubCallback A function to call once the subscription has been cancelled */ - stopSubscription(deviceId: string, stopSubCallback: Function) { + async stopSubscription(deviceId: string, stopSubCallback: Function) { clearInterval(this.#intHandles[deviceId]); delete this.#intHandles[deviceId]; + await this.unsubscribe(this.#subHandles.get(deviceId)); + this.#subHandles.delete(deviceId); stopSubCallback(); } @@ -142,7 +167,7 @@ export abstract class DeviceConnection extends EventEmitter { /** * Device class represents both the proprietary connection and Sparkplug connections for a device */ -export abstract class Device { +export class Device { #spClient: SparkplugNode // The sparkplug client #devConn: DeviceConnection // The associated device connection to this device @@ -236,8 +261,8 @@ export abstract class Device { this.#populateTemplates(options.templates); } // Add default metrics to the device metrics object - // To be populated further by child class as custom manipulations need to take place this._metrics = new Metrics(this._defaultMetrics); + this._metrics.add(options.metrics); // Flag to keep track of device online status this.#isAlive = false; diff --git a/acs-edge/lib/devices/EtherNetIP.ts b/acs-edge/lib/devices/EtherNetIP.ts index a3fb9f76..cbe8b638 100644 --- a/acs-edge/lib/devices/EtherNetIP.ts +++ b/acs-edge/lib/devices/EtherNetIP.ts @@ -116,25 +116,3 @@ export class EtherNetIPConnection extends DeviceConnection { await this.#client.disconnect(); } } - - -/** - * Define the device - */ -export class EtherNetIPDevice extends Device { - #devConn: EtherNetIPConnection - - constructor(spClient: SparkplugNode, devConn: EtherNetIPConnection, options: deviceOptions) { - - // Force fixed buffer for EtherNet/IP connection before calling super - options.payloadFormat = serialisationType.fixedBuffer; - - super(spClient, devConn, options); - - // Assign device connection to class attribute - this.#devConn = devConn; - - // Add metrics from options argument - this._metrics.add(options.metrics); - } -} diff --git a/acs-edge/lib/devices/MQTT.ts b/acs-edge/lib/devices/MQTT.ts index 8d60fa12..6e9a2f4c 100644 --- a/acs-edge/lib/devices/MQTT.ts +++ b/acs-edge/lib/devices/MQTT.ts @@ -91,12 +91,24 @@ export class MQTTConnection extends DeviceConnection { this.emit('data', {}); } - async subscribe(topic: string) { - this.#client.subscribe(topic, (err) => { - if (err) { - console.log(err); - } - }); + async subscribe (addresses: string[]) { + const topics = addresses.filter(t => t); + const granted = await this.#client.subscribeAsync(topics); + const failed = granted + .filter(g => g.qos == 128) + .map(g => g.topic) + .join(", "); + if (failed) + log(`⚠️ Could not subscribe to southbound topics: ${failed}`); + return granted + .filter(g => g.qos != 128) + .map(g => g.topic); + } + + /* This accepts the return value from `subscribe`. */ + async unsubscribe (handle: any) { + const topics = handle as string[]; + await this.#client.unsubscribeAsync(topics); } /** @@ -128,34 +140,3 @@ export class MQTTConnection extends DeviceConnection { } } - -export class MQTTDevice extends Device { - #devConn: MQTTConnection - - constructor(spClient: SparkplugNode, devConn: MQTTConnection, options: deviceOptions) { - super(spClient, devConn, options); - this.#devConn = devConn; - - this._metrics.add(options.metrics); - this._metrics.addresses.forEach((topic) => { - if (topic) this.#devConn.subscribe(topic); - }) - - // Define function for handling data pushed to device asynchronously - // this.#devConn.on("asyncData", async (topic: string, msg: any) => { - // let changedMetrics: sparkplugMetric[] = []; - // this._metrics.getPathsForAddr(topic).forEach((path) => { - // const targetMetric = this._metrics.getByAddrPath(topic, path); - // const newVal = parseValueFromPayload(msg, targetMetric, this._payloadFormat, this._delimiter); - // if (!util.isDeepStrictEqual(targetMetric.value, newVal)) { - // this._metrics.setValueByAddrPath(topic, path, newVal); - // changedMetrics.push(targetMetric); - // } - // }) - // if (changedMetrics.length) { - // this.onConnData(changedMetrics); - // } - // }); - } - -} diff --git a/acs-edge/lib/devices/MTConnect.ts b/acs-edge/lib/devices/MTConnect.ts index 81008489..4b396415 100644 --- a/acs-edge/lib/devices/MTConnect.ts +++ b/acs-edge/lib/devices/MTConnect.ts @@ -9,7 +9,7 @@ import {PassThrough} from 'stream'; import {log} from '../helpers/log.js'; import {Metrics, restConnDetails, serialisationType} from "../helpers/typeHandler.js"; -import {RestConnection, RestDevice} from "./REST.js"; +import {RestConnection} from "./REST.js"; import axios, {CancelTokenSource, CancelTokenStatic} from "axios"; export class MTConnectConnection extends ( @@ -111,35 +111,3 @@ export class MTConnectConnection extends ( this.emit("close"); } }; - - -export class MTConnectDevice extends ( - RestDevice -) { - #devConn: MTConnectConnection - - constructor(spClient: SparkplugNode, devConn: MTConnectConnection, options: deviceOptions) { - super(spClient, devConn, options); - this.#devConn = devConn; - this._payloadFormat = serialisationType.XML; - - this._metrics.add(options.metrics); - - // this.#devConn.on("asyncData", (buffer: Buffer) => { - // let changedMetrics: sparkplugMetric[] = []; - // this._metrics.array.forEach(metric => { - // if (metric.properties.path.value) { - // const newVal = parseValueFromPayload(buffer.toString(), metric, this._payloadFormat, this._delimiter); - // if (!util.isDeepStrictEqual(metric.value, newVal)) { - // this._metrics.setValueByName(metric.name, newVal, Date.now()); - // changedMetrics.push(metric); - // } - // } - // }) - // this.onConnData(changedMetrics); - // }); - //this.#devConn.sample(this._metrics, options.pollInt); - this._isConnected = true; - log(`${this._name} ready`); - } -}; diff --git a/acs-edge/lib/devices/OPCUA.ts b/acs-edge/lib/devices/OPCUA.ts index 6a0d0561..e7fa4087 100644 --- a/acs-edge/lib/devices/OPCUA.ts +++ b/acs-edge/lib/devices/OPCUA.ts @@ -172,7 +172,7 @@ export class OPCUAConnection extends DeviceConnection { if (!isErr) writeCallback(); } - startSubscription( + async startSubscription( metrics: Metrics, payloadFormat: string, delimiter: string, @@ -251,34 +251,3 @@ export class OPCUAConnection extends DeviceConnection { } } } - - -export class OPCUADevice extends (Device) { - devConn: OPCUAConnection - - constructor(spClient: SparkplugNode, devConn: OPCUAConnection, options: deviceOptions) { - super(spClient, devConn, options); - this.devConn = devConn; - - this._metrics.add(options.metrics); - - try { - this.devConn.on('open', () => { - this._isConnected = true; - log(`${this._name} ready`); - }) - - - // this.devConn.on('asyncData', (changedMetrics: changedMetricType) => { - // let updatedMetrics:sparkplugMetric[] = []; - // for (const addr in changedMetrics) { - // this._metrics.setValueByAddrPath(addr, '', changedMetrics[addr]); - // updatedMetrics.push(this._metrics.getByAddrPath(addr, '')); - // } - // this.onConnData(updatedMetrics); - // }) - } catch (e) { - console.log(e); - } - } -} \ No newline at end of file diff --git a/acs-edge/lib/devices/REST.ts b/acs-edge/lib/devices/REST.ts index d326fd1a..873fc449 100644 --- a/acs-edge/lib/devices/REST.ts +++ b/acs-edge/lib/devices/REST.ts @@ -159,20 +159,3 @@ export class RestConnection extends ( async delete() { } }; - - -export class RestDevice extends ( - Device -) { - #devConn: RestConnection - - constructor(spClient: SparkplugNode, devConn: RestConnection, options: deviceOptions) { - super(spClient, devConn, options); - this.#devConn = devConn; - - this._metrics.add(options.metrics); - - this._isConnected = true; - log(`${this._name} ready`); - } -}; \ No newline at end of file diff --git a/acs-edge/lib/devices/S7.ts b/acs-edge/lib/devices/S7.ts index ef15bfa5..fcc76fd7 100644 --- a/acs-edge/lib/devices/S7.ts +++ b/acs-edge/lib/devices/S7.ts @@ -24,7 +24,8 @@ interface s7Vars { export class S7Connection extends DeviceConnection { #s7Conn: typeof S7Endpoint #itemGroup: typeof S7ItemGroup - #vars: s7Vars + /* XXX I'm not sure what purpose this serves */ + #vars: Set constructor(type: string, connDetails: s7ConnDetails) { super(type); @@ -38,8 +39,8 @@ export class S7Connection extends DeviceConnection { }); // Prepare variables to hold optimized metric list - this.#itemGroup = null; - this.#vars = {}; + this.#itemGroup = new S7ItemGroup(this.#s7Conn); + this.#vars = new Set(); // Pass on disconnect event to parent this.#s7Conn.on('disconnect', () => { @@ -59,21 +60,20 @@ export class S7Connection extends DeviceConnection { }) } - /** - * Builds the S7 item group from the defined metric list - * @param {object} vars object containing metric names and PLC addresses - */ - addToItemGroup(vars: s7Vars) { - // If item group doesn't exist, create a fresh setup - if (!this.#itemGroup) { - this.#itemGroup = new S7ItemGroup(this.#s7Conn); - this.#vars = {} - this.#itemGroup.setTranslationCB((metric: string) => this.#vars[metric]); //translates a metric name to its address + async subscribe (addresses: string[]) { + for (const a of addresses) { + this.#vars.add(a); + this.#itemGroup.addItems(a); } - // Merge existing vars with new ones - this.#vars = {...this.#vars, ...vars} - // Add metrics to read for this connection - this.#itemGroup.addItems(Object.keys(this.#vars)); + return addresses; + } + + async unsubscribe (handle: any) { + const addresses = handle as string[]; + for (const a of addresses) { + this.#vars.delete(a); + this.#itemGroup.removeItems(a); + }; } /** @@ -133,7 +133,7 @@ export class S7Connection extends DeviceConnection { */ async close() { // Clear the variable list - this.#vars = {}; + this.#vars = new Set(); // Destroy the metric item group, if it exists if (this.#itemGroup) { this.#itemGroup.destroy(); @@ -146,32 +146,3 @@ export class S7Connection extends DeviceConnection { // !! IMPORTANT !! // Ensure metric addresses are as specified here: // https://github.com/st-one-io/node-red-contrib-s7#variable-addressing - - -/** - * S7 Device class - */ -export class S7Device extends (Device) { - s7Vars: { - [index: string]: string // name: address - } - #devConn: S7Connection - constructor(spClient: SparkplugNode, devConn: S7Connection, options: deviceOptions) { - super(spClient, devConn, options); - this.#devConn = devConn; - this._metrics.add(options.metrics); - // Prepare list of variables for S7 library to use - this.s7Vars = {}; - // Push metric to S7 variables list - options.metrics.forEach((metric) => { - if (typeof metric.properties !== "undefined" && metric.properties.address.value) { - this.s7Vars[metric.properties.address.value as string] = - metric.properties.address.value as string; - } - }); - - // Set S7 variables as item group (this allows optimization of PLC transactions) - this.#devConn.addToItemGroup(this.s7Vars); - - } -} \ No newline at end of file diff --git a/acs-edge/lib/devices/UDP.ts b/acs-edge/lib/devices/UDP.ts index 9b4cfeee..5edc9daf 100644 --- a/acs-edge/lib/devices/UDP.ts +++ b/acs-edge/lib/devices/UDP.ts @@ -80,44 +80,3 @@ export class UDPConnection extends DeviceConnection { } } } - -export class UDPDevice extends (Device) { - #devConn: UDPConnection - constructor(spClient: SparkplugNode, devConn: UDPConnection, options: deviceOptions) { - super(spClient, devConn, options); - this.#devConn = devConn; - - // Prepare metric list - // This is the bare minimum you have to do - this._metrics.add(options.metrics); - - // this.#devConn.on('asyncData', (msg: Buffer, rinfo: RemoteInfo) => { - // let changedMetrics: sparkplugMetric[] = []; - // for (let i = 0; i < this._metrics.length; i++) { - // const metric = this._metrics.array[i]; - // const addr = (metric.properties.address.value as string); - - // // If metric method is GET and a path is set - // // If address is set and it matches the metric or address is not set - // // If port is set and it matches the metric or port is not set - // if (metric.properties.method.value === "GET" - // && metric.properties.path.value) { - // if ((!addr || addr == rinfo.address)) { - // let newVal = parseValueFromPayload(msg, metric, this._payloadFormat, this._delimiter); - // if (!util.isDeepStrictEqual(this._metrics.array[i].value, newVal)) { - // this._metrics.setValueByIndex(i, newVal); - // changedMetrics.push(metric); - // } - // } - // } - // } - // if (changedMetrics.length) { - // this.onConnData(changedMetrics); - // } - // }) - } - - subscribeToMetricChanges() { - // Override with empty method to prevent polling - } -} \ No newline at end of file diff --git a/acs-edge/lib/devices/templateDevice.ts b/acs-edge/lib/devices/templateDevice.ts index dfc036a2..99c392f4 100644 --- a/acs-edge/lib/devices/templateDevice.ts +++ b/acs-edge/lib/devices/templateDevice.ts @@ -39,6 +39,18 @@ export class MyConnection extends DeviceConnection { this.emit("open"); } + async subscribe (addresses: string[]): Promise { + // Here you prepare to read from the requested addresses. Return + // some suitable value that will allow you to undo what you did + // here: this might be the `addresses` array, or it might be + // something else. + return addresses; + } + + async unsubscribe (handle: any): Promise { + // Undo what you did in `subscribe`. + } + /** * * @param metrics Metrics object @@ -74,25 +86,3 @@ export class MyConnection extends DeviceConnection { // Do whatever cleanup code you need to here } }; - - -/** - * Define device for your device type - */ -export class MyDevice extends Device { - // Declare any class attributes and types here - #devConn: MyConnection - constructor(spClient: SparkplugNode, devConn: MyConnection, options: deviceOptions) { - super(spClient, devConn, options); - // Assign device connection to class attribute - this.#devConn = devConn; - - // Add metrics from options argument - // NOTE: You may need to do some preprocessing here - this._metrics.add(options.metrics); - - // When ready, you must set this._isConnected to true to indicate to the parent class - this._isConnected = true; - log(`${this._name} ready`); - } -}; diff --git a/acs-edge/lib/devices/websocket.ts b/acs-edge/lib/devices/websocket.ts index d4d82b33..730d2125 100644 --- a/acs-edge/lib/devices/websocket.ts +++ b/acs-edge/lib/devices/websocket.ts @@ -65,35 +65,3 @@ export class WebsocketConnection extends DeviceConnection { this.#ws.close() } }; - - -export class WebsocketDevice extends Device { - #devConn: WebsocketConnection - constructor(spClient: SparkplugNode, devConn: WebsocketConnection, options: deviceOptions) { - super(spClient, devConn, options); - this.#devConn = devConn; - - this._metrics.add(options.metrics); - - // Define function for handling data pushed to device asynchronously - // this.#devConn.on("asyncData", async (msg: any) => { - // // console.log(msg); - // let changedMetrics: sparkplugMetric[] = []; - // for (let i = 0; i < this._metrics.array.length; i++) { - // const metric = this._metrics.array[i]; - // if (metric.properties.path.value - // && (metric.properties.method.value as string).search(/^GET/g) > -1) { - // const newVal = parseValueFromPayload(msg, metric, this._payloadFormat, this._delimiter); - // if (!util.isDeepStrictEqual(metric.value, newVal)) { - // this._metrics.setValueByIndex(i, newVal); - // changedMetrics.push(this._metrics.array[i]); - // } - // } - // } - // if (changedMetrics.length) { - // this.onConnData(changedMetrics); - // } - // }); - } - -}; diff --git a/acs-edge/lib/translator.ts b/acs-edge/lib/translator.ts index 825d8613..3c2b2567 100644 --- a/acs-edge/lib/translator.ts +++ b/acs-edge/lib/translator.ts @@ -25,14 +25,14 @@ import * as UUIDs from "./uuids.js"; import {log} from "./helpers/log.js"; import {sparkplugConfig,} from "./helpers/typeHandler.js"; -import {RestConnection, RestDevice} from "./devices/REST.js"; -import {S7Connection, S7Device} from "./devices/S7.js"; -import {OPCUAConnection, OPCUADevice} from "./devices/OPCUA.js"; -import {MQTTConnection, MQTTDevice} from "./devices/MQTT.js"; -import {UDPConnection, UDPDevice} from "./devices/UDP.js"; -import {WebsocketConnection, WebsocketDevice} from "./devices/websocket.js"; -import {MTConnectConnection, MTConnectDevice} from "./devices/MTConnect.js"; -import {EtherNetIPConnection, EtherNetIPDevice} from "./devices/EtherNetIP.js"; +import {RestConnection} from "./devices/REST.js"; +import {S7Connection} from "./devices/S7.js"; +import {OPCUAConnection} from "./devices/OPCUA.js"; +import {MQTTConnection} from "./devices/MQTT.js"; +import {UDPConnection} from "./devices/UDP.js"; +import {WebsocketConnection} from "./devices/websocket.js"; +import {MTConnectConnection} from "./devices/MTConnect.js"; +import {EtherNetIPConnection} from "./devices/EtherNetIP.js"; /** * Translator class basically turns config file into instantiated classes @@ -45,7 +45,6 @@ export interface translatorConf { } interface deviceInfo { - type: any, connection: any; connectionDetails: any } @@ -188,10 +187,12 @@ export class Translator extends EventEmitter { } // Instantiate device connection - const newConn = this.connections[cType] = new deviceInfo.connection(connection.connType, connection[deviceInfo.connectionDetails]); + const newConn = this.connections[cType] = new deviceInfo.connection( + connection.connType, connection[deviceInfo.connectionDetails]); connection.devices?.forEach((devConf: deviceOptions) => { - this.devices[devConf.deviceId] = new deviceInfo.type(this.sparkplugNode, newConn, devConf); + this.devices[devConf.deviceId] = new Device( + this.sparkplugNode, newConn, devConf); }); // What to do when the connection is open @@ -226,35 +227,35 @@ export class Translator extends EventEmitter { switch (connType) { case "REST": return { - type: RestDevice, connection: RestConnection, connectionDetails: 'RESTConnDetails' + connection: RestConnection, connectionDetails: 'RESTConnDetails' } case "MTConnect": return { - type: MTConnectDevice, connection: MTConnectConnection, connectionDetails: 'MTConnectConnDetails' + connection: MTConnectConnection, connectionDetails: 'MTConnectConnDetails' } case "EtherNet/IP": return { - type: EtherNetIPDevice, connection: EtherNetIPConnection, connectionDetails: 'EtherNetIPConnDetails' + connection: EtherNetIPConnection, connectionDetails: 'EtherNetIPConnDetails' } case "S7": return { - type: S7Device, connection: S7Connection, connectionDetails: 's7ConnDetails' + connection: S7Connection, connectionDetails: 's7ConnDetails' } case "OPC UA": return { - type: OPCUADevice, connection: OPCUAConnection, connectionDetails: 'OPCUAConnDetails' + connection: OPCUAConnection, connectionDetails: 'OPCUAConnDetails' } case "MQTT": return { - type: MQTTDevice, connection: MQTTConnection, connectionDetails: 'MQTTConnDetails' + connection: MQTTConnection, connectionDetails: 'MQTTConnDetails' } case "Websocket": return { - type: WebsocketDevice, connection: WebsocketConnection, connectionDetails: 'WebsocketConnDetails' + connection: WebsocketConnection, connectionDetails: 'WebsocketConnDetails' } case "UDP": return { - type: UDPDevice, connection: UDPConnection, connectionDetails: 'UDPConnDetails' + connection: UDPConnection, connectionDetails: 'UDPConnDetails' } default: From d89ac3d5aa86bfcfe9b2c2043b16ee50d180d95f Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Wed, 19 Jun 2024 11:40:53 +0100 Subject: [PATCH 09/36] Update TODO --- acs-edge/docs/TODO.md | 54 +++++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/acs-edge/docs/TODO.md b/acs-edge/docs/TODO.md index 0bfdab0a..4cb09a63 100644 --- a/acs-edge/docs/TODO.md +++ b/acs-edge/docs/TODO.md @@ -1,33 +1,37 @@ # TODO list for edge-split work -The Device subclasses need to go. Where they do work this needs to move -into the Connection. In particular some Devices handle subscription -tasks which should move to `startSubscription`. +- [x] The Device subclasses need to go. Where they do work this needs to + move into the Connection. In particular some Devices handle + subscription tasks which should move to `startSubscription`. -`DeviceConnection.readMetrics` accepts payload format / delimiter -arguments. I don't think any of the drivers use them? This belongs -EA-side. +- [ ] `DeviceConnection.readMetrics` accepts payload format / delimiter + arguments. I don't think any of the drivers use them? This belongs + EA-side. -`writeMetrics` also accepts format/delimiter. I'm not clear yet that it -isn't used for this code path. Ideally we want all device writes to -accept a plain Buffer as might be provided from a read. +- [ ] `writeMetrics` also accepts format/delimiter. I'm not clear yet + that it isn't used for this code path. Ideally we want all device + writes to accept a plain Buffer as might be provided from a read. -The Connection currently handles the poll loop, as part of -`startSubscription`. This is implemented entirely in the base class and -should be handled by the Device (EA-side). +- [ ] The Connection currently handles the poll loop, as part of + `startSubscription`. + * For simple connections this is implemented in the base class and + should be handled by the Device (EA-side). + * Some Connections (MTConnect, OPCUA) can request polling in the + southbound protocol. The driver protocol needs extending to handle + this case. -More generally, the Connections shouldn't see the Metrics at all. They -should operator entirely on addresses. +- [ ] More generally, the Connections shouldn't see the Metrics at all. + They should operate entirely on addresses. -Multiple Devices may subscribe to a single Connection. The EA-side -Connection will need to track the current list of addresses we are -interested in and push it down to the driver. +- [ ] Multiple Devices may subscribe to a single Connection. The EA-side + Connection will need to track the current list of addresses we are + interested in and push it down to the driver. -Devices are currently linked to a single Connection. This is not -necessary, but means we need to: -* Add a `connection` property to each metric definition. -* Change the Device to poll via a central connection manifold rather - than via an individual connection. -* Supply data topic names to a Device in return for addresses when it - subscribes to the connection manifold. -* Poll the manifold using connection/datatopic pairs. +- [ ] Devices are currently linked to a single Connection. This is not + necessary, but means we need to: + * Add a `connection` property to each metric definition. + * Change the Device to poll via a central connection manifold rather + than via an individual connection. + * Supply data topic names to a Device in return for addresses when + it subscribes to the connection manifold. + * Poll the manifold using connection/datatopic pairs. From f332d358e98b7af026f06e6060006988bd63f473 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Thu, 20 Jun 2024 11:08:20 +0100 Subject: [PATCH 10/36] Implement a Driver connection type This sits alongside the other connection types, for now, and speaks my new driver protocol. This doesn't handle CMDs or errors correctly yet, but otherwise works. I'm not entirely sure how to handle drivers that want to poll themselves. Currently the EA keeps sending poll messages regardless, and drivers like OPCUA can just not subscribe, and use the group poll instruction instead. I have disabled the watchdog timeout; it fires much too quickly when you're pretending to be a device manually. I'm not sure a fixed timeout with no ping is the right thing to do anyway; there are valid reasons why a connection might not speak for 10s. --- acs-edge/lib/device.ts | 21 ++- acs-edge/lib/devices/driver.ts | 201 ++++++++++++++++++++++++++++ acs-edge/lib/driverBroker.ts | 53 ++++++-- acs-edge/lib/helpers/log.ts | 8 +- acs-edge/lib/helpers/typeHandler.ts | 6 +- acs-edge/lib/sparkplugNode.ts | 8 +- acs-edge/lib/translator.ts | 27 +++- 7 files changed, 292 insertions(+), 32 deletions(-) create mode 100644 acs-edge/lib/devices/driver.ts diff --git a/acs-edge/lib/device.ts b/acs-edge/lib/device.ts index 31c1b8aa..ce109bd3 100644 --- a/acs-edge/lib/device.ts +++ b/acs-edge/lib/device.ts @@ -4,7 +4,7 @@ */ import { - log + log, logf } from "./helpers/log.js"; import * as fs from "fs"; import { @@ -176,7 +176,7 @@ export class Device { _defaultMetrics: sparkplugMetric[] // The default metrics common to all devices #isAlive: boolean // Whether this device is alive or not _isConnected: boolean // Whether this device is ready to publish or not - #deathTimer: ReturnType // A "dead mans handle" or "watchdog" timer which triggers a DDEATH + //#deathTimer: ReturnType // A "dead mans handle" or "watchdog" timer which triggers a DDEATH // if allowed to time out _payloadFormat: serialisationType // The format of the payloads produced by this device _delimiter: string // String specifying the delimiter character if needed @@ -271,9 +271,9 @@ export class Device { // Create watchdog timer which, if allowed to elapse, will set the device as offline // This watchdog is kicked by several read/write functions below - this.#deathTimer = setTimeout(() => { - this.#publishDDeath(); - }, 10000); + //this.#deathTimer = setTimeout(() => { + // this.#publishDDeath(); + //}, 10000); //What to do when the device is ready //We Just need to sub to metric changes @@ -291,16 +291,19 @@ export class Device { } _handleData(obj: { [p: string]: any }, parseVals: boolean) { + logf("_handleData %s (%s) %O", this._name, parseVals, obj); // Array to keep track of values that changed let changedMetrics: sparkplugMetric[] = []; // Iterate through each key in obj for (let addr in obj) { // Get all payload paths registered for this address const paths = this._metrics.getPathsForAddr(addr); + logf("paths for %s: %s", addr, paths); // Iterate through each path paths.forEach((path) => { // Get the complete metric according to its address and path const metric = this._metrics.getByAddrPath(addr, path); + logf("metric for %s:%s: %O", addr, path, metric); // If the metric can be read i.e. GET method if (typeof metric.properties !== "undefined" && (metric.properties.method.value as string).search( /^GET/g) > -1) { @@ -315,6 +318,8 @@ export class Device { this._delimiter ) : obj[addr]; + logf("parsed new val: %O", newVal); + // Test if the value is a bigint and convert it to a Long. This is a hack to ensure that the // Tahu library works - it only accepts Longs, not bigints. if (typeof newVal === "bigint") { @@ -334,6 +339,8 @@ export class Device { this._payloadFormat, this._delimiter ); + logf("updating metric %s:%s ts %s val %O", + addr, path, timestamp, newVal); // Update the metric value and push it to the array of changed metrics changedMetrics.push(this._metrics.setValueByAddrPath(addr, path, newVal, timestamp)); @@ -355,7 +362,7 @@ export class Device { // Kick the watchdog timer to prevent the device dying _refreshDeathTimer() { // Reset timeout to it's initial value - this.#deathTimer.refresh(); + //this.#deathTimer.refresh(); } /** @@ -512,7 +519,7 @@ export class Device { this._stopMetricSubscription(); // Stop the watchdog timer so that we can instantly stop - clearTimeout(this.#deathTimer); + //clearTimeout(this.#deathTimer); } diff --git a/acs-edge/lib/devices/driver.ts b/acs-edge/lib/devices/driver.ts new file mode 100644 index 00000000..9a02e931 --- /dev/null +++ b/acs-edge/lib/devices/driver.ts @@ -0,0 +1,201 @@ +/* + * Factory+ / AMRC Connectivity Stack (ACS) Edge component + * Copyright 2023 AMRC + */ + +import * as util from "util"; +import Long from "long"; + +import { Metrics, serialisationType } from "../helpers/typeHandler.js"; +import { log } from "../helpers/log.js"; + +import { DriverBroker } from "../driverBroker.js"; +import { DeviceConnection } from "../device.js"; + +interface addrGroup { + poll: number, + addrs: Set, +} + +export class DriverConnection extends DeviceConnection { + id: string + conf: any + broker: DriverBroker + status: string + addrs: Map + topics: Map + groups: Map + + constructor(type: string, details: any, name: string, broker: DriverBroker) { + // Call constructor of parent class + super(type); + this.id = name; + this.conf = details; + this.broker = broker; + + this.status = "DOWN"; + this.addrs = new Map(); + this.topics = new Map(); + this.groups = new Map(); + } + + open() { + log(`Opening Driver ${this.id}`); + this.broker.on("message", this.#message.bind(this)); + this.broker.publish({ + id: this.id, + msg: "active", + payload: Buffer.from("ONLINE"), + }); + /* We do not emit "open" here, we wait for the negotiation with + * the driver. */ + } + + close () { + this.broker.publish({ + id: this.id, + msg: "active", + payload: Buffer.from("OFFLINE"), + }); + this.broker.off("message", this.#message.bind(this)); + this.emit("close"); + } + + + + readMetrics(metrics: Metrics, payloadFormat?: string, delimiter?: string) { + const poll = metrics.addresses + .filter(a => this.topics.has(a)) + .map(a => this.topics.get(a)) + .join("\n"); + this.broker.publish({ + id: this.id, + msg: "poll", + payload: Buffer.from(poll), + }); + } + + writeMetrics(metrics: Metrics, writeCallback: Function, payloadFormat: serialisationType, delimiter?: string) { + let err = null; + // Do whatever connection specific stuff you need to in order to + // write to the device + + // Call the writeCallback when complete, setting the error if + // necessary + writeCallback(err); + } + + /** + * + * @param metrics Metrics object to watch + * @param payloadFormat String denoting the format of the payload + * @param delimiter String specifying the delimiter character if needed + * @param interval Time interval between metric reads, in ms + * @param deviceId The device ID whose metrics are to be watched + * @param subscriptionStartCallback A function to call once the subscription has been setup + */ + async startSubscription(metrics: Metrics, payloadFormat: serialisationType, delimiter: string, interval: number, deviceId: string, subscriptionStartCallback: Function) { + const addrs = metrics.addresses; + const unassigned = addrs.filter(a => !this.topics.has(a)); + for (const a of unassigned) { + const dt = this.#newTopic(); + this.addrs.set(dt, a); + this.topics.set(a, dt); + } + const topics = new Set(addrs.map(a => this.topics.get(a)!)) + this.groups.set(deviceId, { + poll: interval, + addrs: topics, + }); + + if (this.status != "DOWN") + this.#send_addrs(); + + super.startSubscription(metrics, payloadFormat, delimiter, interval, + deviceId, subscriptionStartCallback); + } + + /** + * Stop a previously registered subscription for metric changes. + * @param deviceId The device ID we are cancelling the subscription for + * @param stopSubCallback A function to call once the subscription has been cancelled + */ + //async stopSubscription(deviceId: string, stopSubCallback: Function) { + //} + + #newTopic () { + while (true) { + const dt = Math.floor(Math.random() * 100000).toString(); + if (!this.addrs.has(dt)) + return dt; + } + } + + #message (message) { + const { id, msg, data, payload } = message; + if (id != this.id) return; + + log(util.format("DRIVER message: %s %s", id, msg)); + switch (msg) { + case "status": return this.#msg_status(payload.toString()); + case "data": return this.#msg_data(data, payload); + case "err": return this.#msg_err(data, payload.toString()); + + /* Our messages are looped */ + case "active": + case "conf": + case "addr": + case "cmd": + case "poll": + return; + } + log(`Unexpected ${msg} message from ${id}`); + } + + #msg_status (status: string) { + this.status = status; + log(`DRIVER [${this.id}]: status ${status}`); + switch (status) { + case "READY": + this.broker.publish({ + id: this.id, + msg: "conf", + payload: Buffer.from(JSON.stringify(this.conf)), + }); + this.#send_addrs(); + break; + case "UP": + this.emit("open"); + break; + case "DOWN": + this.emit("close"); + break; + } + } + + #send_addrs () { + const addrs = { + addrs: Object.fromEntries(this.addrs), + groups: Object.fromEntries( + [...this.groups.entries()] + .map(([n, i]) => [n, { + poll: i.poll, + addrs: [...i.addrs], + }])), + }; + this.broker.publish({ + id: this.id, + msg: "addr", + payload: Buffer.from(JSON.stringify(addrs)), + }); + } + + #msg_data (data: string, payload: Buffer) { + const addr = this.addrs.get(data); + log(`Driver [${this.id}]: data ${data} ${addr}`); + if (addr) + this.emit("data", { [addr]: payload }); + } + + #msg_err (data: string, error: string) { } +} diff --git a/acs-edge/lib/driverBroker.ts b/acs-edge/lib/driverBroker.ts index 9e0c45e3..fb3b0219 100644 --- a/acs-edge/lib/driverBroker.ts +++ b/acs-edge/lib/driverBroker.ts @@ -18,11 +18,15 @@ function log (f, ...a) { console.log("DRIVER: %s", msg); } +interface ACL { + publish: RegExp, + subscribe: RegExp, +} + export class DriverBroker extends EventEmitter { broker: Aedes passwords: string - publish: Map - subscribe: Map + acl: Map hostname: string port: number @@ -42,8 +46,7 @@ export class DriverBroker extends EventEmitter { this.passwords = env.EDGE_PASSWORDS; this.broker = new Aedes(); - this.publish = new Map(); - this.subscribe = new Map(); + this.acl = new Map(); const br = this.broker; br.authenticate = this.auth.bind(this); @@ -58,9 +61,13 @@ export class DriverBroker extends EventEmitter { start () { const srv = net.createServer(this.broker.handle); - srv.on("listening", () => - log("Listening: %o", srv.address())); - srv.listen(this.port, this.hostname); + return new Promise(resolve => { + srv.once("listening", () => { + log("Listening: %o", srv.address()); + resolve(); + }); + srv.listen(this.port, this.hostname); + }); } stop () { @@ -84,11 +91,13 @@ export class DriverBroker extends EventEmitter { return fail("Unexpected driver %s", username); if (expect.compare(password) != 0) return fail("Bad password for %s", username); - - this.publish.set(id, new RegExp( - `^${prefix}/${id}/(?:status|data/\\w+|err/\\w+)$`)); - this.subscribe.set(id, new RegExp( - `^${prefix}/${id}/(?:conf|addr|cmd/\\w+|poll)$`)); + + this.acl.set(id, { + publish: new RegExp( + `^${prefix}/${id}/(?:status|data/\\w+|err/\\w+)$`), + subscribe: new RegExp( + `^${prefix}/${id}/(?:active|conf|addr|cmd/\\w+|poll)$`), + }); callback(null, true); } @@ -100,7 +109,7 @@ export class DriverBroker extends EventEmitter { log("PUBLISH: %s %s", id, topic); if (packet.retain) return callback(new Error("Retained PUBLISH forbidden")); - if (!this.publish.get(id)!.test(topic)) + if (!this.acl.get(id)!.publish.test(topic)) return callback(new Error("Unauthorised PUBLISH")); callback(null); } @@ -110,7 +119,7 @@ export class DriverBroker extends EventEmitter { const { topic } = subscription; log("SUBSCRIBE: %s %s", id, topic); - if (!this.subscribe.get(id)!.test(topic)) + if (!this.acl.get(id)!.subscribe.test(topic)) return callback(new Error("Unauthorised SUBSCRIBE")); callback(null, subscription); } @@ -127,4 +136,20 @@ export class DriverBroker extends EventEmitter { const [, id, msg, data] = match; this.emit("message", { id, msg, data, payload }); } + + publish (packet: { id, msg, data?, payload }): Promise { + const { id, msg, data, payload } = packet; + + const topic = `${prefix}/${id}/${msg}` + + (data ? `/${data}` : ""); + log("Publishing %s: %O", topic, packet); + return new Promise((resolve, reject) => + this.broker.publish({ + cmd: "publish", + qos: 0, + dup: false, + retain: false, + topic, payload, + }, err => err ? reject(err) : resolve())); + } } diff --git a/acs-edge/lib/helpers/log.ts b/acs-edge/lib/helpers/log.ts index 6476bda2..fef7755e 100644 --- a/acs-edge/lib/helpers/log.ts +++ b/acs-edge/lib/helpers/log.ts @@ -3,6 +3,8 @@ * Copyright 2023 AMRC */ +import util from "util"; + import * as dotenv from 'dotenv' dotenv.config(); @@ -22,4 +24,8 @@ export function log(msg: String) { } } } -} \ No newline at end of file +} + +export function logf (fmt, ...args) { + log(util.format(fmt, ...args)); +} diff --git a/acs-edge/lib/helpers/typeHandler.ts b/acs-edge/lib/helpers/typeHandler.ts index c40eb882..9fe48d7f 100644 --- a/acs-edge/lib/helpers/typeHandler.ts +++ b/acs-edge/lib/helpers/typeHandler.ts @@ -10,7 +10,7 @@ import * as jsonpointer from 'jsonpointer'; import * as Long from "long"; import {MessageSecurityMode, SecurityPolicy} from "node-opcua"; import {Address} from "@amrc-factoryplus/utilities"; -import {log} from "./log.js"; +import {log, logf} from "./log.js"; export enum serialisationType { ignored = "Defined by Protocol", @@ -356,6 +356,8 @@ export class Metrics { export function parseValueFromPayload(msg: any, metric: sparkplugMetric, payloadFormat: serialisationType | string, delimiter?: string) { let payload: any; const path = (typeof metric.properties !== "undefined" && typeof metric.properties.path !== "undefined" ? metric.properties.path.value as string : ""); + logf("parseValueFP: path [%s], fmt [%s], type [%s], msg [%o]", + path, payloadFormat, metric.type, msg); switch (payloadFormat) { case serialisationType.delimited: // Handle no delimiter @@ -596,6 +598,8 @@ export function typeLens(type: string): number { */ export function parseValFromBuffer(type: sparkplugDataType, endianness: byteOrder, byteAddr: number, buf: Buffer, bit?: number): any { + logf("parseValFB: addr %s, bit %s, end %s, typ %s, buf: %O", + byteAddr, bit, endianness, type, buf); switch (type) { case sparkplugDataType.boolean: if (bit != null) { diff --git a/acs-edge/lib/sparkplugNode.ts b/acs-edge/lib/sparkplugNode.ts index f429e430..16bdd492 100644 --- a/acs-edge/lib/sparkplugNode.ts +++ b/acs-edge/lib/sparkplugNode.ts @@ -12,7 +12,7 @@ import { ServiceClient, } from "@amrc-factoryplus/utilities"; -import { log } from "./helpers/log.js"; +import { log, logf } from "./helpers/log.js"; import { metricIndex, Metrics, @@ -412,8 +412,10 @@ export class SparkplugNode extends ( */ async #handleNCmd(payload: sparkplugPayload) { // For each metric in payload - log('Handling NCMD'); - console.log(payload); + logf('Handling NCMD: %O', { + ...payload, + body: payload.body?.toString(), + }); await Promise.all( payload.metrics.map(async (metric: sparkplugMetric) => { // If metric only has an alias, find it's name diff --git a/acs-edge/lib/translator.ts b/acs-edge/lib/translator.ts index 3c2b2567..32da38a8 100644 --- a/acs-edge/lib/translator.ts +++ b/acs-edge/lib/translator.ts @@ -33,6 +33,7 @@ import {UDPConnection} from "./devices/UDP.js"; import {WebsocketConnection} from "./devices/websocket.js"; import {MTConnectConnection} from "./devices/MTConnect.js"; import {EtherNetIPConnection} from "./devices/EtherNetIP.js"; +import {DriverConnection} from "./devices/driver.js"; /** * Translator class basically turns config file into instantiated classes @@ -92,6 +93,11 @@ export class Translator extends EventEmitter { this.sparkplugNode = await new SparkplugNode(this.fplus, spConf).init(); log(`Created Sparkplug node "${ids.sparkplug!}".`); + log("Starting driver broker..."); + this.broker.on("message", msg => + log(util.format("Driver message: %O", msg))); + await this.broker.start(); + // Create a new device connection for each type listed in config file log('Building up connections and devices...'); conf?.deviceConnections?.forEach(c => this.setupConnection(c)); @@ -99,10 +105,6 @@ export class Translator extends EventEmitter { // Setup Sparkplug node handlers this.setupSparkplug(); - log("Starting driver broker..."); - this.broker.on("message", msg => - log(util.format("Driver message: %O", msg))); - this.broker.start(); } catch (e: any) { log(`Error starting translator: ${e.message}`); console.error((e as Error).stack); @@ -188,7 +190,13 @@ export class Translator extends EventEmitter { // Instantiate device connection const newConn = this.connections[cType] = new deviceInfo.connection( - connection.connType, connection[deviceInfo.connectionDetails]); + connection.connType, + connection[deviceInfo.connectionDetails], + /* XXX These additional parameters are a hack for now to + * make the DriverConnection work. They want refactoring + * later. */ + connection.name, + this.broker); connection.devices?.forEach((devConf: deviceOptions) => { this.devices[devConf.deviceId] = new Device( @@ -204,6 +212,8 @@ export class Translator extends EventEmitter { // What to do when the device connection has new data from a device newConn.on('data', (obj: { [index: string]: any }, parseVals = true) => { + log(util.format("Received data for %s: (%s) %O", + connection.name, parseVals, obj)); connection.devices?.forEach((devConf: deviceOptions) => { this.devices[devConf.deviceId]?._handleData(obj, parseVals); }) @@ -258,6 +268,11 @@ export class Translator extends EventEmitter { connection: UDPConnection, connectionDetails: 'UDPConnDetails' } + case "Driver": + return { + connection: DriverConnection, + connectionDetails: "DriverDetails", + }; default: return; } @@ -320,7 +335,7 @@ export class Translator extends EventEmitter { if (valid) { try { config = JSON.parse(secretReplacedConfig); - valid = validateConfig(config); + //valid = validateConfig(config); } catch { valid = false; } From 0b103865adc87cd76b24ccd41f84bf676c154d34 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Thu, 20 Jun 2024 11:33:33 +0100 Subject: [PATCH 11/36] Update TODO --- acs-edge/docs/TODO.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/acs-edge/docs/TODO.md b/acs-edge/docs/TODO.md index 4cb09a63..8c5844e6 100644 --- a/acs-edge/docs/TODO.md +++ b/acs-edge/docs/TODO.md @@ -12,7 +12,7 @@ that it isn't used for this code path. Ideally we want all device writes to accept a plain Buffer as might be provided from a read. -- [ ] The Connection currently handles the poll loop, as part of +- [x] The Connection currently handles the poll loop, as part of `startSubscription`. * For simple connections this is implemented in the base class and should be handled by the Device (EA-side). @@ -23,7 +23,7 @@ - [ ] More generally, the Connections shouldn't see the Metrics at all. They should operate entirely on addresses. -- [ ] Multiple Devices may subscribe to a single Connection. The EA-side +- [x] Multiple Devices may subscribe to a single Connection. The EA-side Connection will need to track the current list of addresses we are interested in and push it down to the driver. From 4c54234e153698ffe8c05d41d4c565c1282584e4 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Thu, 27 Jun 2024 12:46:56 +0100 Subject: [PATCH 12/36] Create a test driver This accepts addresses specifying fixed functions. We need to supply a `version` in the address config. --- acs-edge/Makefile | 1 + acs-edge/docs/TODO.md | 2 + acs-edge/lib/devices/driver.ts | 1 + edge-test/.gitignore | 1 + edge-test/bin/driver.js | 120 +++++ edge-test/package-lock.json | 797 +++++++++++++++++++++++++++++++++ edge-test/package.json | 17 + 7 files changed, 939 insertions(+) create mode 100644 edge-test/.gitignore create mode 100644 edge-test/bin/driver.js create mode 100644 edge-test/package-lock.json create mode 100644 edge-test/package.json diff --git a/acs-edge/Makefile b/acs-edge/Makefile index a1e06c11..979616cf 100644 --- a/acs-edge/Makefile +++ b/acs-edge/Makefile @@ -7,6 +7,7 @@ repo?=acs-edge include ${mk}/acs.js.mk local.build: + npm install npx tsc --project tsconfig.json local.run: local.build diff --git a/acs-edge/docs/TODO.md b/acs-edge/docs/TODO.md index 8c5844e6..759b762a 100644 --- a/acs-edge/docs/TODO.md +++ b/acs-edge/docs/TODO.md @@ -35,3 +35,5 @@ * Supply data topic names to a Device in return for addresses when it subscribes to the connection manifold. * Poll the manifold using connection/datatopic pairs. + * Possibly the Device should always assume we are using a 'smart' + driver, and polling should be handled by the manifold? diff --git a/acs-edge/lib/devices/driver.ts b/acs-edge/lib/devices/driver.ts index 9a02e931..5ef943b4 100644 --- a/acs-edge/lib/devices/driver.ts +++ b/acs-edge/lib/devices/driver.ts @@ -175,6 +175,7 @@ export class DriverConnection extends DeviceConnection { #send_addrs () { const addrs = { + version: 1, addrs: Object.fromEntries(this.addrs), groups: Object.fromEntries( [...this.groups.entries()] diff --git a/edge-test/.gitignore b/edge-test/.gitignore new file mode 100644 index 00000000..3c3629e6 --- /dev/null +++ b/edge-test/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/edge-test/bin/driver.js b/edge-test/bin/driver.js new file mode 100644 index 00000000..0da40e99 --- /dev/null +++ b/edge-test/bin/driver.js @@ -0,0 +1,120 @@ +/* AMRC Connectivity Stack + * Modbus Edge Agent driver + * Copyright 2024 AMRC + */ + +import asyncjs from "async"; +import MQTT from "mqtt"; + +const id = process.env.EDGE_USERNAME; + +const topic = (msg, data) => `fpEdge1/${id}/${msg}` + (data ? `/${data}` : ""); + +const mqtt = MQTT.connect({ + url: process.env.EDGE_MQTT, + clientId: id, + username: id, + password: process.env.EDGE_PASSWORD, + will: { + topic: topic("status"), + payload: "DOWN", + }, + manualConnect: true, + resubscribe: false, +}); + +let status = "DOWN"; +function setStatus (st) { + status = st; + if (mqtt.connected) + mqtt.publish(topic("status"), status); +} +let conf; +let addrs; + +const funcs = { + const: (p, a) => t => a, + sin: (p, a) => t => a * Math.sin(2 * Math.PI * (t / p)), + saw: (p, a) => t => (a / p) * (t % p), +} + +function parseAddr (addr) { + const parts = addr.split(":"); + if (parts.length != 3) return; + + const func = funcs[parts[0]]; + if (!func) return; + const period = Number.parseFloat(parts[1]); + if (Number.isNaN(period)) return; + const amplitude = Number.parseFloat(parts[2]); + if (Number.isNaN(amplitude)) return; + + return func(period, amplitude); +} + +function setAddrs (pkt) { + if (pkt.version != 1) { + console.log("Bad addr config version: %s", pkt.version); + return false; + } + + const parsed = Object.entries(pkt.addrs) + .map(([t, a]) => [t, parseAddr(a)]); + if (parsed.some(([, f]) => !f)) { + console.log("BAD ADDRS: %O", pkt.addrs); + return false; + } + + addrs = new Map( + parsed.map(([t, f]) => [t, { topic: t, func: f }])); + console.log("Set addrs: %O", addrs); + return true; +} + +const polling = asyncjs.queue(async addr => { + const val = addr.func(performance.now()); + const buf = Buffer.alloc(8); + const top = topic("data", addr.topic); + + buf.writeDoubleBE(val); + await mqtt.publishAsync(top, buf); +}); + +mqtt.on("connect", async () => { + await mqtt.subscribeAsync( + "active conf addr poll" + .split(" ") + .map(t => topic(t))); + setStatus("READY"); +}); +mqtt.on("message", (t, p) => { + const [, , msg, data] = t.split("/"); + switch (msg) { + case "active": + if (p.toString() == "ONLINE") + setStatus("READY"); + break; + case "conf": + conf = JSON.parse(p.toString()); + console.log("CONF: %O", conf); + setStatus("UP"); + break; + case "addr": + const a = JSON.parse(p.toString()); + if (!setAddrs(a)) + setStatus("CONF"); + break; + case "poll": + const poll = p.toString().split("\n"); + console.log("POLL %O", poll); + if (addrs) + poll.map(t => addrs.get(t)) + .filter(a => a) + .forEach(a => polling.push(a)); + else + console.log("Can't poll, no addrs!"); + break; + } +}); + +mqtt.connect(); diff --git a/edge-test/package-lock.json b/edge-test/package-lock.json new file mode 100644 index 00000000..b553adfa --- /dev/null +++ b/edge-test/package-lock.json @@ -0,0 +1,797 @@ +{ + "name": "edge-modbus", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "edge-modbus", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "async": "^3.2.5", + "mqtt": "^5.7.2" + } + }, + "node_modules/@babel/runtime": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@types/node": { + "version": "20.14.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.6.tgz", + "integrity": "sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/readable-stream": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.14.tgz", + "integrity": "sha512-xZn/AuUbCMShGsqH/ehZtGDwQtbx00M9rZ2ENLe4tOjFZ/JFeWMhEZkk2fEe1jAUqqEAURIkFJ7Az/go8mM1/w==", + "dependencies": { + "@types/node": "*", + "safe-buffer": "~5.1.1" + } + }, + "node_modules/@types/ws": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bl": { + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.0.12.tgz", + "integrity": "sha512-EnEYHilP93oaOa2MnmNEjAcovPS3JlQZOyzGXi3EyEpPhm9qWvdDp7BmAVEVusGzp8LlwQK56Av+OkDoRjzE0w==", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/commist": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz", + "integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-unique-numbers": { + "version": "8.0.13", + "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-8.0.13.tgz", + "integrity": "sha512-7OnTFAVPefgw2eBJ1xj2PGGR9FwYzSUso9decayHgCDX4sJkHLdcsYTytTg+tYv+wKF3U8gJuSBz2jJpQV4u/g==", + "dependencies": { + "@babel/runtime": "^7.23.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.1.0" + } + }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/js-sdsl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", + "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/lru-cache": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mqtt": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.7.2.tgz", + "integrity": "sha512-b5xIA9J/K1LTubSWKaNYYLxYIusQdip6o9/8bRWad2TelRr8xLifjQt+SnamDAwMp3O6NdvR9E8ae7VMuN02kg==", + "dependencies": { + "@types/readable-stream": "^4.0.5", + "@types/ws": "^8.5.9", + "commist": "^3.2.0", + "concat-stream": "^2.0.0", + "debug": "^4.3.4", + "help-me": "^5.0.0", + "lru-cache": "^10.0.1", + "minimist": "^1.2.8", + "mqtt": "^5.2.0", + "mqtt-packet": "^9.0.0", + "number-allocator": "^1.0.14", + "readable-stream": "^4.4.2", + "reinterval": "^1.1.0", + "rfdc": "^1.3.0", + "split2": "^4.2.0", + "worker-timers": "^7.1.4", + "ws": "^8.17.1" + }, + "bin": { + "mqtt": "build/bin/mqtt.js", + "mqtt_pub": "build/bin/pub.js", + "mqtt_sub": "build/bin/sub.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/mqtt-packet": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.0.tgz", + "integrity": "sha512-8v+HkX+fwbodsWAZIZTI074XIoxVBOmPeggQuDFCGg1SqNcC+uoRMWu7J6QlJPqIUIJXmjNYYHxBBLr1Y/Df4w==", + "dependencies": { + "bl": "^6.0.8", + "debug": "^4.3.4", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/number-allocator": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz", + "integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==", + "dependencies": { + "debug": "^4.3.1", + "js-sdsl": "4.3.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/reinterval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", + "integrity": "sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==" + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/worker-timers": { + "version": "7.1.8", + "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-7.1.8.tgz", + "integrity": "sha512-R54psRKYVLuzff7c1OTFcq/4Hue5Vlz4bFtNEIarpSiCYhpifHU3aIQI29S84o1j87ePCYqbmEJPqwBTf+3sfw==", + "dependencies": { + "@babel/runtime": "^7.24.5", + "tslib": "^2.6.2", + "worker-timers-broker": "^6.1.8", + "worker-timers-worker": "^7.0.71" + } + }, + "node_modules/worker-timers-broker": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-6.1.8.tgz", + "integrity": "sha512-FUCJu9jlK3A8WqLTKXM9E6kAmI/dR1vAJ8dHYLMisLNB/n3GuaFIjJ7pn16ZcD1zCOf7P6H62lWIEBi+yz/zQQ==", + "dependencies": { + "@babel/runtime": "^7.24.5", + "fast-unique-numbers": "^8.0.13", + "tslib": "^2.6.2", + "worker-timers-worker": "^7.0.71" + } + }, + "node_modules/worker-timers-worker": { + "version": "7.0.71", + "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-7.0.71.tgz", + "integrity": "sha512-ks/5YKwZsto1c2vmljroppOKCivB/ma97g9y77MAAz2TBBjPPgpoOiS1qYQKIgvGTr2QYPT3XhJWIB6Rj2MVPQ==", + "dependencies": { + "@babel/runtime": "^7.24.5", + "tslib": "^2.6.2" + } + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + }, + "dependencies": { + "@babel/runtime": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, + "@types/node": { + "version": "20.14.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.6.tgz", + "integrity": "sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw==", + "requires": { + "undici-types": "~5.26.4" + } + }, + "@types/readable-stream": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.14.tgz", + "integrity": "sha512-xZn/AuUbCMShGsqH/ehZtGDwQtbx00M9rZ2ENLe4tOjFZ/JFeWMhEZkk2fEe1jAUqqEAURIkFJ7Az/go8mM1/w==", + "requires": { + "@types/node": "*", + "safe-buffer": "~5.1.1" + } + }, + "@types/ws": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "requires": { + "@types/node": "*" + } + }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, + "async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "bl": { + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.0.12.tgz", + "integrity": "sha512-EnEYHilP93oaOa2MnmNEjAcovPS3JlQZOyzGXi3EyEpPhm9qWvdDp7BmAVEVusGzp8LlwQK56Av+OkDoRjzE0w==", + "requires": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "commist": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz", + "integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==" + }, + "concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "requires": { + "ms": "2.1.2" + } + }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + }, + "fast-unique-numbers": { + "version": "8.0.13", + "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-8.0.13.tgz", + "integrity": "sha512-7OnTFAVPefgw2eBJ1xj2PGGR9FwYzSUso9decayHgCDX4sJkHLdcsYTytTg+tYv+wKF3U8gJuSBz2jJpQV4u/g==", + "requires": { + "@babel/runtime": "^7.23.8", + "tslib": "^2.6.2" + } + }, + "help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==" + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "js-sdsl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", + "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==" + }, + "lru-cache": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==" + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + }, + "mqtt": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.7.2.tgz", + "integrity": "sha512-b5xIA9J/K1LTubSWKaNYYLxYIusQdip6o9/8bRWad2TelRr8xLifjQt+SnamDAwMp3O6NdvR9E8ae7VMuN02kg==", + "requires": { + "@types/readable-stream": "^4.0.5", + "@types/ws": "^8.5.9", + "commist": "^3.2.0", + "concat-stream": "^2.0.0", + "debug": "^4.3.4", + "help-me": "^5.0.0", + "lru-cache": "^10.0.1", + "minimist": "^1.2.8", + "mqtt": "^5.2.0", + "mqtt-packet": "^9.0.0", + "number-allocator": "^1.0.14", + "readable-stream": "^4.4.2", + "reinterval": "^1.1.0", + "rfdc": "^1.3.0", + "split2": "^4.2.0", + "worker-timers": "^7.1.4", + "ws": "^8.17.1" + } + }, + "mqtt-packet": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.0.tgz", + "integrity": "sha512-8v+HkX+fwbodsWAZIZTI074XIoxVBOmPeggQuDFCGg1SqNcC+uoRMWu7J6QlJPqIUIJXmjNYYHxBBLr1Y/Df4w==", + "requires": { + "bl": "^6.0.8", + "debug": "^4.3.4", + "process-nextick-args": "^2.0.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "number-allocator": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz", + "integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==", + "requires": { + "debug": "^4.3.1", + "js-sdsl": "4.3.0" + } + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, + "regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "reinterval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", + "integrity": "sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==" + }, + "rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, + "tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "worker-timers": { + "version": "7.1.8", + "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-7.1.8.tgz", + "integrity": "sha512-R54psRKYVLuzff7c1OTFcq/4Hue5Vlz4bFtNEIarpSiCYhpifHU3aIQI29S84o1j87ePCYqbmEJPqwBTf+3sfw==", + "requires": { + "@babel/runtime": "^7.24.5", + "tslib": "^2.6.2", + "worker-timers-broker": "^6.1.8", + "worker-timers-worker": "^7.0.71" + } + }, + "worker-timers-broker": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-6.1.8.tgz", + "integrity": "sha512-FUCJu9jlK3A8WqLTKXM9E6kAmI/dR1vAJ8dHYLMisLNB/n3GuaFIjJ7pn16ZcD1zCOf7P6H62lWIEBi+yz/zQQ==", + "requires": { + "@babel/runtime": "^7.24.5", + "fast-unique-numbers": "^8.0.13", + "tslib": "^2.6.2", + "worker-timers-worker": "^7.0.71" + } + }, + "worker-timers-worker": { + "version": "7.0.71", + "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-7.0.71.tgz", + "integrity": "sha512-ks/5YKwZsto1c2vmljroppOKCivB/ma97g9y77MAAz2TBBjPPgpoOiS1qYQKIgvGTr2QYPT3XhJWIB6Rj2MVPQ==", + "requires": { + "@babel/runtime": "^7.24.5", + "tslib": "^2.6.2" + } + }, + "ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "requires": {} + } + } +} diff --git a/edge-test/package.json b/edge-test/package.json new file mode 100644 index 00000000..c03bd6db --- /dev/null +++ b/edge-test/package.json @@ -0,0 +1,17 @@ +{ + "name": "edge-modbus", + "version": "1.0.0", + "description": "", + "main": "index.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "async": "^3.2.5", + "mqtt": "^5.7.2" + } +} From 4a4351fc1ebd23f716a318dadfea6b9d52a41338 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Thu, 11 Jul 2024 10:41:00 +0100 Subject: [PATCH 13/36] Support variable data packing --- edge-test/bin/driver.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/edge-test/bin/driver.js b/edge-test/bin/driver.js index 0da40e99..c431e1ce 100644 --- a/edge-test/bin/driver.js +++ b/edge-test/bin/driver.js @@ -36,11 +36,17 @@ const funcs = { const: (p, a) => t => a, sin: (p, a) => t => a * Math.sin(2 * Math.PI * (t / p)), saw: (p, a) => t => (a / p) * (t % p), -} +}; +const packing = { + bd: [8, (b, v) => b.writeDoubleBE(v)], + ld: [8, (b, v) => b.writeDoubleLE(v)], + bf: [4, (b, v) => b.writeFloatBE(v)], + lf: [4, (b, v) => b.writeFloatLE(v)], +}; function parseAddr (addr) { const parts = addr.split(":"); - if (parts.length != 3) return; + if (parts.length != 4) return; const func = funcs[parts[0]]; if (!func) return; @@ -48,8 +54,10 @@ function parseAddr (addr) { if (Number.isNaN(period)) return; const amplitude = Number.parseFloat(parts[2]); if (Number.isNaN(amplitude)) return; + const pack = packing[parts[3]]; + if (!pack) return; - return func(period, amplitude); + return [func(period, amplitude), ...pack]; } function setAddrs (pkt) { @@ -66,17 +74,18 @@ function setAddrs (pkt) { } addrs = new Map( - parsed.map(([t, f]) => [t, { topic: t, func: f }])); + parsed.map(([t, [f, s, p]]) => + [t, { topic: t, func: f, size: s, pack: p }])); console.log("Set addrs: %O", addrs); return true; } const polling = asyncjs.queue(async addr => { const val = addr.func(performance.now()); - const buf = Buffer.alloc(8); + const buf = Buffer.alloc(addr.size); const top = topic("data", addr.topic); - buf.writeDoubleBE(val); + addr.pack(buf, val); await mqtt.publishAsync(top, buf); }); From 7b5e4d53b22a4233f43c6bd2766b2458f6948741 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Thu, 11 Jul 2024 14:32:09 +0100 Subject: [PATCH 14/36] Create a Modbus driver This doesn't handle errors entirely properly yet, but it works. --- edge-modbus/.gitignore | 1 + edge-modbus/bin/driver.js | 152 ++++ edge-modbus/package-lock.json | 1254 +++++++++++++++++++++++++++++++++ edge-modbus/package.json | 21 + 4 files changed, 1428 insertions(+) create mode 100644 edge-modbus/.gitignore create mode 100644 edge-modbus/bin/driver.js create mode 100644 edge-modbus/package-lock.json create mode 100644 edge-modbus/package.json diff --git a/edge-modbus/.gitignore b/edge-modbus/.gitignore new file mode 100644 index 00000000..3c3629e6 --- /dev/null +++ b/edge-modbus/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/edge-modbus/bin/driver.js b/edge-modbus/bin/driver.js new file mode 100644 index 00000000..b49936f5 --- /dev/null +++ b/edge-modbus/bin/driver.js @@ -0,0 +1,152 @@ +/* AMRC Connectivity Stack + * Modbus Edge Agent driver + * Copyright 2024 AMRC + */ + +import asyncjs from "async"; +import MQTT from "mqtt"; +import ModbusRTU from "modbus-serial"; + +const id = process.env.EDGE_USERNAME; + +const topic = (msg, data) => `fpEdge1/${id}/${msg}` + (data ? `/${data}` : ""); + +const mqtt = MQTT.connect({ + url: process.env.EDGE_MQTT, + clientId: id, + username: id, + password: process.env.EDGE_PASSWORD, + will: { + topic: topic("status"), + payload: "DOWN", + }, + manualConnect: true, + resubscribe: false, +}); + +let status = "DOWN"; +function setStatus (st) { + status = st; + if (mqtt.connected) + mqtt.publish(topic("status"), status); +} + +let client; +async function setConf (conf) { + if (conf.protocol != "tcp") + return "CONF"; + + client = new ModbusRTU(); + return client.connectTCP(conf.host, { port: conf.port }) + .then(() => "UP", () => "CONN"); +} + +let addrs; + +const funcs = { + input: { + read: (c, a, l) => c.readInputRegisters(a, l), + }, + holding: { + read: (c, a, l) => c.readHoldingRegisters(a, l), + }, + coil: { + read: (c, a, l) => c.readCoils(a, l), + }, + discrete: { + read: (c, a, l) => c.readDiscreteInputs(a, l), + }, +}; + +function parseAddr (spec) { + const parts = spec.split(","); + if (parts.length != 4) return; + + const id = Number.parseInt(parts[0]); + if (Number.isNaN(id) || id < 0) return; + const func = funcs[parts[1]]; + if (!func) return; + const addr = Number.parseInt(parts[2]); + if (Number.isNaN(addr) || addr < 0) return; + const len = Number.parseInt(parts[3]); + if (Number.isNaN(len) || len < 1) return; + + return { id, func, addr, len }; +} + +const polling = asyncjs.queue(async ({ data, addr }) => { + console.log("READ %O", addr); + if (!client) { + console.log("Can't poll, no client!"); + return; + } + if (!client.isOpen) + await new Promise((r, j) => + client.open(e => e ? j(e) : r())); + + client.setID(addr.id); + const val = await addr.func.read(client, addr.addr, addr.len); + console.log("DATA %O", val); + + await mqtt.publishAsync(data, val.buffer); +}); + +function setAddrs (pkt) { + if (pkt.version != 1) { + console.log("Bad addr config version: %s", pkt.version); + return false; + } + + const parsed = Object.entries(pkt.addrs) + .map(([t, a]) => [t, parseAddr(a)]); + if (parsed.some(([, f]) => !f)) { + console.log("BAD ADDRS: %O", pkt.addrs); + return false; + } + + addrs = new Map(parsed); + console.log("Set addrs: %O", addrs); + return true; +} + +mqtt.on("connect", async () => { + await mqtt.subscribeAsync( + "active conf addr poll" + .split(" ") + .map(t => topic(t))); + setStatus("READY"); +}); +mqtt.on("message", async (t, p) => { + const [, , msg, data] = t.split("/"); + switch (msg) { + case "active": + if (p.toString() == "ONLINE") + setStatus("READY"); + break; + case "conf": + const conf = JSON.parse(p.toString()); + console.log("CONF: %O", conf); + setStatus(await setConf(conf)); + break; + case "addr": + const a = JSON.parse(p.toString()); + if (!setAddrs(a)) + setStatus("CONF"); + break; + case "poll": + const poll = p.toString().split("\n"); + console.log("POLL %s %O", polling.length(), poll); + if (addrs) + poll.map(t => ({ + data: topic("data", t), + addr: addrs.get(t), + })) + .filter(({ addr }) => addr) + .forEach(a => polling.push(a)); + else + console.log("Can't poll, no addrs!"); + break; + } +}); + +mqtt.connect(); diff --git a/edge-modbus/package-lock.json b/edge-modbus/package-lock.json new file mode 100644 index 00000000..a0734d48 --- /dev/null +++ b/edge-modbus/package-lock.json @@ -0,0 +1,1254 @@ +{ + "name": "amrc-connectivity-stack", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "amrc-connectivity-stack", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "async": "^3.2.5", + "modbus-serial": "^8.0.17", + "mqtt": "^5.8.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@serialport/binding-mock": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-10.2.2.tgz", + "integrity": "sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==", + "dependencies": { + "@serialport/bindings-interface": "^1.2.1", + "debug": "^4.3.3" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@serialport/bindings-cpp": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@serialport/bindings-cpp/-/bindings-cpp-12.0.1.tgz", + "integrity": "sha512-r2XOwY2dDvbW7dKqSPIk2gzsr6M6Qpe9+/Ngs94fNaNlcTRCV02PfaoDmRgcubpNVVcLATlxSxPTIDw12dbKOg==", + "hasInstallScript": true, + "dependencies": { + "@serialport/bindings-interface": "1.2.2", + "@serialport/parser-readline": "11.0.0", + "debug": "4.3.4", + "node-addon-api": "7.0.0", + "node-gyp-build": "4.6.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/bindings-cpp/node_modules/@serialport/parser-delimiter": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-11.0.0.tgz", + "integrity": "sha512-aZLJhlRTjSmEwllLG7S4J8s8ctRAS0cbvCpO87smLvl3e4BgzbVgF6Z6zaJd3Aji2uSiYgfedCdNc4L6W+1E2g==", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/bindings-cpp/node_modules/@serialport/parser-readline": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-11.0.0.tgz", + "integrity": "sha512-rRAivhRkT3YO28WjmmG4FQX6L+KMb5/ikhyylRfzWPw0nSXy97+u07peS9CbHqaNvJkMhH1locp2H36aGMOEIA==", + "dependencies": { + "@serialport/parser-delimiter": "11.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/bindings-cpp/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@serialport/bindings-interface": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@serialport/bindings-interface/-/bindings-interface-1.2.2.tgz", + "integrity": "sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==", + "engines": { + "node": "^12.22 || ^14.13 || >=16" + } + }, + "node_modules/@serialport/parser-byte-length": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-12.0.0.tgz", + "integrity": "sha512-0ei0txFAj+s6FTiCJFBJ1T2hpKkX8Md0Pu6dqMrYoirjPskDLJRgZGLqoy3/lnU1bkvHpnJO+9oJ3PB9v8rNlg==", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-cctalk": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-12.0.0.tgz", + "integrity": "sha512-0PfLzO9t2X5ufKuBO34DQKLXrCCqS9xz2D0pfuaLNeTkyGUBv426zxoMf3rsMRodDOZNbFblu3Ae84MOQXjnZw==", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-delimiter": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-12.0.0.tgz", + "integrity": "sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-inter-byte-timeout": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-12.0.0.tgz", + "integrity": "sha512-GnCh8K0NAESfhCuXAt+FfBRz1Cf9CzIgXfp7SdMgXwrtuUnCC/yuRTUFWRvuzhYKoAo1TL0hhUo77SFHUH1T/w==", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-packet-length": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-packet-length/-/parser-packet-length-12.0.0.tgz", + "integrity": "sha512-p1hiCRqvGHHLCN/8ZiPUY/G0zrxd7gtZs251n+cfNTn+87rwcdUeu9Dps3Aadx30/sOGGFL6brIRGK4l/t7MuQ==", + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@serialport/parser-readline": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-12.0.0.tgz", + "integrity": "sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==", + "dependencies": { + "@serialport/parser-delimiter": "12.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-ready": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-12.0.0.tgz", + "integrity": "sha512-ygDwj3O4SDpZlbrRUraoXIoIqb8sM7aMKryGjYTIF0JRnKeB1ys8+wIp0RFMdFbO62YriUDextHB5Um5cKFSWg==", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-regex": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-12.0.0.tgz", + "integrity": "sha512-dCAVh4P/pZrLcPv9NJ2mvPRBg64L5jXuiRxIlyxxdZGH4WubwXVXY/kBTihQmiAMPxbT3yshSX8f2+feqWsxqA==", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-slip-encoder": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-slip-encoder/-/parser-slip-encoder-12.0.0.tgz", + "integrity": "sha512-0APxDGR9YvJXTRfY+uRGhzOhTpU5akSH183RUcwzN7QXh8/1jwFsFLCu0grmAUfi+fItCkR+Xr1TcNJLR13VNA==", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-spacepacket": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-spacepacket/-/parser-spacepacket-12.0.0.tgz", + "integrity": "sha512-dozONxhPC/78pntuxpz/NOtVps8qIc/UZzdc/LuPvVsqCoJXiRxOg6ZtCP/W58iibJDKPZPAWPGYeZt9DJxI+Q==", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/stream": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-12.0.0.tgz", + "integrity": "sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==", + "dependencies": { + "@serialport/bindings-interface": "1.2.2", + "debug": "4.3.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/stream/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@types/node": { + "version": "20.14.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", + "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/readable-stream": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.15.tgz", + "integrity": "sha512-oAZ3kw+kJFkEqyh7xORZOku1YAKvsFTogRY8kVl4vHpEKiDkfnSA/My8haRE7fvmix5Zyy+1pwzOi7yycGLBJw==", + "dependencies": { + "@types/node": "*", + "safe-buffer": "~5.1.1" + } + }, + "node_modules/@types/ws": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bl": { + "version": "6.0.14", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.0.14.tgz", + "integrity": "sha512-TJfbvGdL7KFGxTsEbsED7avqpFdY56q9IW0/aiytyheJzxST/+Io6cx/4Qx0K2/u0BPRDs65mjaQzYvMZeNocQ==", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/commist": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz", + "integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-unique-numbers": { + "version": "8.0.13", + "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-8.0.13.tgz", + "integrity": "sha512-7OnTFAVPefgw2eBJ1xj2PGGR9FwYzSUso9decayHgCDX4sJkHLdcsYTytTg+tYv+wKF3U8gJuSBz2jJpQV4u/g==", + "dependencies": { + "@babel/runtime": "^7.23.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.1.0" + } + }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/js-sdsl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", + "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/modbus-serial": { + "version": "8.0.17", + "resolved": "https://registry.npmjs.org/modbus-serial/-/modbus-serial-8.0.17.tgz", + "integrity": "sha512-1lCNpCY72wTgGnnBzv9O3ZfbKeHl9zZTgVuMgp2mEqFErEZC7p3I0CGpzfQ4XPHT0Aqz+qXhpkGew8WK57SwvQ==", + "dependencies": { + "debug": "^4.3.1", + "serialport": "^12.0.0" + } + }, + "node_modules/mqtt": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.8.0.tgz", + "integrity": "sha512-/+H04mv6goy6K5gHMNH3uS0icBzXapS+4uUf4yZyQWXi72APPZNb81bQhvkm99poEQettXVT8XETB0mPxl5Wjg==", + "dependencies": { + "@types/readable-stream": "^4.0.5", + "@types/ws": "^8.5.9", + "commist": "^3.2.0", + "concat-stream": "^2.0.0", + "debug": "^4.3.4", + "help-me": "^5.0.0", + "lru-cache": "^10.0.1", + "minimist": "^1.2.8", + "mqtt": "^5.2.0", + "mqtt-packet": "^9.0.0", + "number-allocator": "^1.0.14", + "readable-stream": "^4.4.2", + "reinterval": "^1.1.0", + "rfdc": "^1.3.0", + "split2": "^4.2.0", + "worker-timers": "^7.1.4", + "ws": "^8.17.1" + }, + "bin": { + "mqtt": "build/bin/mqtt.js", + "mqtt_pub": "build/bin/pub.js", + "mqtt_sub": "build/bin/sub.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/mqtt-packet": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.0.tgz", + "integrity": "sha512-8v+HkX+fwbodsWAZIZTI074XIoxVBOmPeggQuDFCGg1SqNcC+uoRMWu7J6QlJPqIUIJXmjNYYHxBBLr1Y/Df4w==", + "dependencies": { + "bl": "^6.0.8", + "debug": "^4.3.4", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/node-addon-api": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.0.0.tgz", + "integrity": "sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==" + }, + "node_modules/node-gyp-build": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", + "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/number-allocator": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz", + "integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==", + "dependencies": { + "debug": "^4.3.1", + "js-sdsl": "4.3.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/reinterval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", + "integrity": "sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==" + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/serialport": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/serialport/-/serialport-12.0.0.tgz", + "integrity": "sha512-AmH3D9hHPFmnF/oq/rvigfiAouAKyK/TjnrkwZRYSFZxNggJxwvbAbfYrLeuvq7ktUdhuHdVdSjj852Z55R+uA==", + "dependencies": { + "@serialport/binding-mock": "10.2.2", + "@serialport/bindings-cpp": "12.0.1", + "@serialport/parser-byte-length": "12.0.0", + "@serialport/parser-cctalk": "12.0.0", + "@serialport/parser-delimiter": "12.0.0", + "@serialport/parser-inter-byte-timeout": "12.0.0", + "@serialport/parser-packet-length": "12.0.0", + "@serialport/parser-readline": "12.0.0", + "@serialport/parser-ready": "12.0.0", + "@serialport/parser-regex": "12.0.0", + "@serialport/parser-slip-encoder": "12.0.0", + "@serialport/parser-spacepacket": "12.0.0", + "@serialport/stream": "12.0.0", + "debug": "4.3.4" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/serialport/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/worker-timers": { + "version": "7.1.8", + "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-7.1.8.tgz", + "integrity": "sha512-R54psRKYVLuzff7c1OTFcq/4Hue5Vlz4bFtNEIarpSiCYhpifHU3aIQI29S84o1j87ePCYqbmEJPqwBTf+3sfw==", + "dependencies": { + "@babel/runtime": "^7.24.5", + "tslib": "^2.6.2", + "worker-timers-broker": "^6.1.8", + "worker-timers-worker": "^7.0.71" + } + }, + "node_modules/worker-timers-broker": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-6.1.8.tgz", + "integrity": "sha512-FUCJu9jlK3A8WqLTKXM9E6kAmI/dR1vAJ8dHYLMisLNB/n3GuaFIjJ7pn16ZcD1zCOf7P6H62lWIEBi+yz/zQQ==", + "dependencies": { + "@babel/runtime": "^7.24.5", + "fast-unique-numbers": "^8.0.13", + "tslib": "^2.6.2", + "worker-timers-worker": "^7.0.71" + } + }, + "node_modules/worker-timers-worker": { + "version": "7.0.71", + "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-7.0.71.tgz", + "integrity": "sha512-ks/5YKwZsto1c2vmljroppOKCivB/ma97g9y77MAAz2TBBjPPgpoOiS1qYQKIgvGTr2QYPT3XhJWIB6Rj2MVPQ==", + "dependencies": { + "@babel/runtime": "^7.24.5", + "tslib": "^2.6.2" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + }, + "dependencies": { + "@babel/runtime": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, + "@serialport/binding-mock": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-10.2.2.tgz", + "integrity": "sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==", + "requires": { + "@serialport/bindings-interface": "^1.2.1", + "debug": "^4.3.3" + } + }, + "@serialport/bindings-cpp": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@serialport/bindings-cpp/-/bindings-cpp-12.0.1.tgz", + "integrity": "sha512-r2XOwY2dDvbW7dKqSPIk2gzsr6M6Qpe9+/Ngs94fNaNlcTRCV02PfaoDmRgcubpNVVcLATlxSxPTIDw12dbKOg==", + "requires": { + "@serialport/bindings-interface": "1.2.2", + "@serialport/parser-readline": "11.0.0", + "debug": "4.3.4", + "node-addon-api": "7.0.0", + "node-gyp-build": "4.6.0" + }, + "dependencies": { + "@serialport/parser-delimiter": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-11.0.0.tgz", + "integrity": "sha512-aZLJhlRTjSmEwllLG7S4J8s8ctRAS0cbvCpO87smLvl3e4BgzbVgF6Z6zaJd3Aji2uSiYgfedCdNc4L6W+1E2g==" + }, + "@serialport/parser-readline": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-11.0.0.tgz", + "integrity": "sha512-rRAivhRkT3YO28WjmmG4FQX6L+KMb5/ikhyylRfzWPw0nSXy97+u07peS9CbHqaNvJkMhH1locp2H36aGMOEIA==", + "requires": { + "@serialport/parser-delimiter": "11.0.0" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + } + } + }, + "@serialport/bindings-interface": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@serialport/bindings-interface/-/bindings-interface-1.2.2.tgz", + "integrity": "sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==" + }, + "@serialport/parser-byte-length": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-12.0.0.tgz", + "integrity": "sha512-0ei0txFAj+s6FTiCJFBJ1T2hpKkX8Md0Pu6dqMrYoirjPskDLJRgZGLqoy3/lnU1bkvHpnJO+9oJ3PB9v8rNlg==" + }, + "@serialport/parser-cctalk": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-12.0.0.tgz", + "integrity": "sha512-0PfLzO9t2X5ufKuBO34DQKLXrCCqS9xz2D0pfuaLNeTkyGUBv426zxoMf3rsMRodDOZNbFblu3Ae84MOQXjnZw==" + }, + "@serialport/parser-delimiter": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-12.0.0.tgz", + "integrity": "sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==" + }, + "@serialport/parser-inter-byte-timeout": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-12.0.0.tgz", + "integrity": "sha512-GnCh8K0NAESfhCuXAt+FfBRz1Cf9CzIgXfp7SdMgXwrtuUnCC/yuRTUFWRvuzhYKoAo1TL0hhUo77SFHUH1T/w==" + }, + "@serialport/parser-packet-length": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-packet-length/-/parser-packet-length-12.0.0.tgz", + "integrity": "sha512-p1hiCRqvGHHLCN/8ZiPUY/G0zrxd7gtZs251n+cfNTn+87rwcdUeu9Dps3Aadx30/sOGGFL6brIRGK4l/t7MuQ==" + }, + "@serialport/parser-readline": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-12.0.0.tgz", + "integrity": "sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==", + "requires": { + "@serialport/parser-delimiter": "12.0.0" + } + }, + "@serialport/parser-ready": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-12.0.0.tgz", + "integrity": "sha512-ygDwj3O4SDpZlbrRUraoXIoIqb8sM7aMKryGjYTIF0JRnKeB1ys8+wIp0RFMdFbO62YriUDextHB5Um5cKFSWg==" + }, + "@serialport/parser-regex": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-12.0.0.tgz", + "integrity": "sha512-dCAVh4P/pZrLcPv9NJ2mvPRBg64L5jXuiRxIlyxxdZGH4WubwXVXY/kBTihQmiAMPxbT3yshSX8f2+feqWsxqA==" + }, + "@serialport/parser-slip-encoder": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-slip-encoder/-/parser-slip-encoder-12.0.0.tgz", + "integrity": "sha512-0APxDGR9YvJXTRfY+uRGhzOhTpU5akSH183RUcwzN7QXh8/1jwFsFLCu0grmAUfi+fItCkR+Xr1TcNJLR13VNA==" + }, + "@serialport/parser-spacepacket": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-spacepacket/-/parser-spacepacket-12.0.0.tgz", + "integrity": "sha512-dozONxhPC/78pntuxpz/NOtVps8qIc/UZzdc/LuPvVsqCoJXiRxOg6ZtCP/W58iibJDKPZPAWPGYeZt9DJxI+Q==" + }, + "@serialport/stream": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-12.0.0.tgz", + "integrity": "sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==", + "requires": { + "@serialport/bindings-interface": "1.2.2", + "debug": "4.3.4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + } + } + }, + "@types/node": { + "version": "20.14.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", + "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", + "requires": { + "undici-types": "~5.26.4" + } + }, + "@types/readable-stream": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.15.tgz", + "integrity": "sha512-oAZ3kw+kJFkEqyh7xORZOku1YAKvsFTogRY8kVl4vHpEKiDkfnSA/My8haRE7fvmix5Zyy+1pwzOi7yycGLBJw==", + "requires": { + "@types/node": "*", + "safe-buffer": "~5.1.1" + } + }, + "@types/ws": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "requires": { + "@types/node": "*" + } + }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, + "async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "bl": { + "version": "6.0.14", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.0.14.tgz", + "integrity": "sha512-TJfbvGdL7KFGxTsEbsED7avqpFdY56q9IW0/aiytyheJzxST/+Io6cx/4Qx0K2/u0BPRDs65mjaQzYvMZeNocQ==", + "requires": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "commist": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz", + "integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==" + }, + "concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "requires": { + "ms": "2.1.2" + } + }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + }, + "fast-unique-numbers": { + "version": "8.0.13", + "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-8.0.13.tgz", + "integrity": "sha512-7OnTFAVPefgw2eBJ1xj2PGGR9FwYzSUso9decayHgCDX4sJkHLdcsYTytTg+tYv+wKF3U8gJuSBz2jJpQV4u/g==", + "requires": { + "@babel/runtime": "^7.23.8", + "tslib": "^2.6.2" + } + }, + "help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==" + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "js-sdsl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", + "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==" + }, + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + }, + "modbus-serial": { + "version": "8.0.17", + "resolved": "https://registry.npmjs.org/modbus-serial/-/modbus-serial-8.0.17.tgz", + "integrity": "sha512-1lCNpCY72wTgGnnBzv9O3ZfbKeHl9zZTgVuMgp2mEqFErEZC7p3I0CGpzfQ4XPHT0Aqz+qXhpkGew8WK57SwvQ==", + "requires": { + "debug": "^4.3.1", + "serialport": "^12.0.0" + } + }, + "mqtt": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.8.0.tgz", + "integrity": "sha512-/+H04mv6goy6K5gHMNH3uS0icBzXapS+4uUf4yZyQWXi72APPZNb81bQhvkm99poEQettXVT8XETB0mPxl5Wjg==", + "requires": { + "@types/readable-stream": "^4.0.5", + "@types/ws": "^8.5.9", + "commist": "^3.2.0", + "concat-stream": "^2.0.0", + "debug": "^4.3.4", + "help-me": "^5.0.0", + "lru-cache": "^10.0.1", + "minimist": "^1.2.8", + "mqtt": "^5.2.0", + "mqtt-packet": "^9.0.0", + "number-allocator": "^1.0.14", + "readable-stream": "^4.4.2", + "reinterval": "^1.1.0", + "rfdc": "^1.3.0", + "split2": "^4.2.0", + "worker-timers": "^7.1.4", + "ws": "^8.17.1" + } + }, + "mqtt-packet": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.0.tgz", + "integrity": "sha512-8v+HkX+fwbodsWAZIZTI074XIoxVBOmPeggQuDFCGg1SqNcC+uoRMWu7J6QlJPqIUIJXmjNYYHxBBLr1Y/Df4w==", + "requires": { + "bl": "^6.0.8", + "debug": "^4.3.4", + "process-nextick-args": "^2.0.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node-addon-api": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.0.0.tgz", + "integrity": "sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==" + }, + "node-gyp-build": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", + "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==" + }, + "number-allocator": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz", + "integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==", + "requires": { + "debug": "^4.3.1", + "js-sdsl": "4.3.0" + } + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, + "regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "reinterval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", + "integrity": "sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==" + }, + "rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "serialport": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/serialport/-/serialport-12.0.0.tgz", + "integrity": "sha512-AmH3D9hHPFmnF/oq/rvigfiAouAKyK/TjnrkwZRYSFZxNggJxwvbAbfYrLeuvq7ktUdhuHdVdSjj852Z55R+uA==", + "requires": { + "@serialport/binding-mock": "10.2.2", + "@serialport/bindings-cpp": "12.0.1", + "@serialport/parser-byte-length": "12.0.0", + "@serialport/parser-cctalk": "12.0.0", + "@serialport/parser-delimiter": "12.0.0", + "@serialport/parser-inter-byte-timeout": "12.0.0", + "@serialport/parser-packet-length": "12.0.0", + "@serialport/parser-readline": "12.0.0", + "@serialport/parser-ready": "12.0.0", + "@serialport/parser-regex": "12.0.0", + "@serialport/parser-slip-encoder": "12.0.0", + "@serialport/parser-spacepacket": "12.0.0", + "@serialport/stream": "12.0.0", + "debug": "4.3.4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + } + } + }, + "split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, + "tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "worker-timers": { + "version": "7.1.8", + "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-7.1.8.tgz", + "integrity": "sha512-R54psRKYVLuzff7c1OTFcq/4Hue5Vlz4bFtNEIarpSiCYhpifHU3aIQI29S84o1j87ePCYqbmEJPqwBTf+3sfw==", + "requires": { + "@babel/runtime": "^7.24.5", + "tslib": "^2.6.2", + "worker-timers-broker": "^6.1.8", + "worker-timers-worker": "^7.0.71" + } + }, + "worker-timers-broker": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-6.1.8.tgz", + "integrity": "sha512-FUCJu9jlK3A8WqLTKXM9E6kAmI/dR1vAJ8dHYLMisLNB/n3GuaFIjJ7pn16ZcD1zCOf7P6H62lWIEBi+yz/zQQ==", + "requires": { + "@babel/runtime": "^7.24.5", + "fast-unique-numbers": "^8.0.13", + "tslib": "^2.6.2", + "worker-timers-worker": "^7.0.71" + } + }, + "worker-timers-worker": { + "version": "7.0.71", + "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-7.0.71.tgz", + "integrity": "sha512-ks/5YKwZsto1c2vmljroppOKCivB/ma97g9y77MAAz2TBBjPPgpoOiS1qYQKIgvGTr2QYPT3XhJWIB6Rj2MVPQ==", + "requires": { + "@babel/runtime": "^7.24.5", + "tslib": "^2.6.2" + } + }, + "ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "requires": {} + } + } +} diff --git a/edge-modbus/package.json b/edge-modbus/package.json new file mode 100644 index 00000000..5692f866 --- /dev/null +++ b/edge-modbus/package.json @@ -0,0 +1,21 @@ +{ + "name": "amrc-connectivity-stack", + "version": "1.0.0", + "description": "The AMRC Connectivity Stack (ACS) is a comprehensive collection of open-source services developed by the AMRC that represents a complete end-to-end implementation of the [Factory+](https://factoryplus.app.amrc.co.uk) framework. It is distributed as a Kubernetes Helm chart an can be deployed onto any Kubernetes cluster.", + "main": "index.js", + "type": "module", + "directories": { + "doc": "docs" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "async": "^3.2.5", + "modbus-serial": "^8.0.17", + "mqtt": "^5.8.0" + } +} From e50fae6aa3f9b289835ee9f2b51626e45ff82f85 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Thu, 11 Jul 2024 14:48:43 +0100 Subject: [PATCH 15/36] Remove some logging --- acs-edge/lib/device.ts | 7 ------- acs-edge/lib/devices/driver.ts | 4 ++-- acs-edge/lib/driverBroker.ts | 6 +++--- acs-edge/lib/helpers/typeHandler.ts | 4 ---- acs-edge/lib/translator.ts | 10 +++++----- 5 files changed, 10 insertions(+), 21 deletions(-) diff --git a/acs-edge/lib/device.ts b/acs-edge/lib/device.ts index bd9d74cd..4b4e60da 100644 --- a/acs-edge/lib/device.ts +++ b/acs-edge/lib/device.ts @@ -286,19 +286,16 @@ export class Device { } _handleData(obj: { [p: string]: any }, parseVals: boolean) { - logf("_handleData %s (%s) %O", this._name, parseVals, obj); // Array to keep track of values that changed let changedMetrics: sparkplugMetric[] = []; // Iterate through each key in obj for (let addr in obj) { // Get all payload paths registered for this address const paths = this._metrics.getPathsForAddr(addr); - logf("paths for %s: %s", addr, paths); // Iterate through each path paths.forEach((path) => { // Get the complete metric according to its address and path const metric = this._metrics.getByAddrPath(addr, path); - logf("metric for %s:%s: %O", addr, path, metric); // If the metric can be read i.e. GET method if (typeof metric.properties !== "undefined" && (metric.properties.method.value as string).search( /^GET/g) > -1) { @@ -313,8 +310,6 @@ export class Device { this._delimiter ) : obj[addr]; - logf("parsed new val: %O", newVal); - // Test if the value is a bigint and convert it to a Long. This is a hack to ensure that the // Tahu library works - it only accepts Longs, not bigints. if (typeof newVal === "bigint") { @@ -334,8 +329,6 @@ export class Device { this._payloadFormat, this._delimiter ); - logf("updating metric %s:%s ts %s val %O", - addr, path, timestamp, newVal); // Update the metric value and push it to the array of changed metrics changedMetrics.push({...(this._metrics.setValueByAddrPath(addr, diff --git a/acs-edge/lib/devices/driver.ts b/acs-edge/lib/devices/driver.ts index 5ef943b4..0fe8f125 100644 --- a/acs-edge/lib/devices/driver.ts +++ b/acs-edge/lib/devices/driver.ts @@ -135,7 +135,7 @@ export class DriverConnection extends DeviceConnection { const { id, msg, data, payload } = message; if (id != this.id) return; - log(util.format("DRIVER message: %s %s", id, msg)); + //log(util.format("DRIVER message: %s %s", id, msg)); switch (msg) { case "status": return this.#msg_status(payload.toString()); case "data": return this.#msg_data(data, payload); @@ -193,7 +193,7 @@ export class DriverConnection extends DeviceConnection { #msg_data (data: string, payload: Buffer) { const addr = this.addrs.get(data); - log(`Driver [${this.id}]: data ${data} ${addr}`); + //log(`Driver [${this.id}]: data ${data} ${addr}`); if (addr) this.emit("data", { [addr]: payload }); } diff --git a/acs-edge/lib/driverBroker.ts b/acs-edge/lib/driverBroker.ts index fb3b0219..1276b763 100644 --- a/acs-edge/lib/driverBroker.ts +++ b/acs-edge/lib/driverBroker.ts @@ -106,7 +106,7 @@ export class DriverBroker extends EventEmitter { const { id } = client; const { topic } = packet; - log("PUBLISH: %s %s", id, topic); + //log("PUBLISH: %s %s", id, topic); if (packet.retain) return callback(new Error("Retained PUBLISH forbidden")); if (!this.acl.get(id)!.publish.test(topic)) @@ -125,7 +125,7 @@ export class DriverBroker extends EventEmitter { } message (topic, payload) { - log("PACKET: %s %o", topic, payload); + //log("PACKET: %s %o", topic, payload); const match = topic.match(topicrx); if (!match) { @@ -142,7 +142,7 @@ export class DriverBroker extends EventEmitter { const topic = `${prefix}/${id}/${msg}` + (data ? `/${data}` : ""); - log("Publishing %s: %O", topic, packet); + //log("Publishing %s: %O", topic, packet); return new Promise((resolve, reject) => this.broker.publish({ cmd: "publish", diff --git a/acs-edge/lib/helpers/typeHandler.ts b/acs-edge/lib/helpers/typeHandler.ts index 9fe48d7f..18dc10b8 100644 --- a/acs-edge/lib/helpers/typeHandler.ts +++ b/acs-edge/lib/helpers/typeHandler.ts @@ -356,8 +356,6 @@ export class Metrics { export function parseValueFromPayload(msg: any, metric: sparkplugMetric, payloadFormat: serialisationType | string, delimiter?: string) { let payload: any; const path = (typeof metric.properties !== "undefined" && typeof metric.properties.path !== "undefined" ? metric.properties.path.value as string : ""); - logf("parseValueFP: path [%s], fmt [%s], type [%s], msg [%o]", - path, payloadFormat, metric.type, msg); switch (payloadFormat) { case serialisationType.delimited: // Handle no delimiter @@ -598,8 +596,6 @@ export function typeLens(type: string): number { */ export function parseValFromBuffer(type: sparkplugDataType, endianness: byteOrder, byteAddr: number, buf: Buffer, bit?: number): any { - logf("parseValFB: addr %s, bit %s, end %s, typ %s, buf: %O", - byteAddr, bit, endianness, type, buf); switch (type) { case sparkplugDataType.boolean: if (bit != null) { diff --git a/acs-edge/lib/translator.ts b/acs-edge/lib/translator.ts index 32da38a8..ed703602 100644 --- a/acs-edge/lib/translator.ts +++ b/acs-edge/lib/translator.ts @@ -94,8 +94,8 @@ export class Translator extends EventEmitter { log(`Created Sparkplug node "${ids.sparkplug!}".`); log("Starting driver broker..."); - this.broker.on("message", msg => - log(util.format("Driver message: %O", msg))); + //this.broker.on("message", msg => + // log(util.format("Driver message: %O", msg))); await this.broker.start(); // Create a new device connection for each type listed in config file @@ -212,8 +212,8 @@ export class Translator extends EventEmitter { // What to do when the device connection has new data from a device newConn.on('data', (obj: { [index: string]: any }, parseVals = true) => { - log(util.format("Received data for %s: (%s) %O", - connection.name, parseVals, obj)); + //log(util.format("Received data for %s: (%s) %O", + // connection.name, parseVals, obj)); connection.devices?.forEach((devConf: deviceOptions) => { this.devices[devConf.deviceId]?._handleData(obj, parseVals); }) @@ -375,4 +375,4 @@ Trying again in ${interval} seconds...`); await timers.setTimeout(interval * 1000); } } -} \ No newline at end of file +} From 547d870145b7e780b89fdd2f85feae18b682b41c Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Fri, 12 Jul 2024 08:55:01 +0100 Subject: [PATCH 16/36] Handle driver connection status properly We need to emit "open" and "close" on the Driver connection, but only if this is a change of status. --- acs-edge/lib/devices/driver.ts | 14 +++++++++++--- edge-modbus/bin/driver.js | 18 +++++++++++++----- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/acs-edge/lib/devices/driver.ts b/acs-edge/lib/devices/driver.ts index 0fe8f125..944ae821 100644 --- a/acs-edge/lib/devices/driver.ts +++ b/acs-edge/lib/devices/driver.ts @@ -153,8 +153,10 @@ export class DriverConnection extends DeviceConnection { } #msg_status (status: string) { + const ost = this.status; this.status = status; - log(`DRIVER [${this.id}]: status ${status}`); + log(`DRIVER [${this.id}]: status ${ost} -> ${status}`); + switch (status) { case "READY": this.broker.publish({ @@ -165,10 +167,16 @@ export class DriverConnection extends DeviceConnection { this.#send_addrs(); break; case "UP": - this.emit("open"); + if (ost != "UP") + this.emit("open"); break; case "DOWN": - this.emit("close"); + case "CONF": + case "CONN": + case "AUTH": + case "ERR": + if (ost == "UP") + this.emit("close"); break; } } diff --git a/edge-modbus/bin/driver.js b/edge-modbus/bin/driver.js index b49936f5..1ebe83f1 100644 --- a/edge-modbus/bin/driver.js +++ b/edge-modbus/bin/driver.js @@ -74,22 +74,30 @@ function parseAddr (spec) { return { id, func, addr, len }; } -const polling = asyncjs.queue(async ({ data, addr }) => { +async function poll ({ data, addr }) { console.log("READ %O", addr); if (!client) { console.log("Can't poll, no client!"); return; } - if (!client.isOpen) - await new Promise((r, j) => - client.open(e => e ? j(e) : r())); + if (!client.isOpen) { + const st = await new Promise((r, j) => + client.open(e => { + console.log("Modbus open: %s", e); + r(e ? "CONN" : "UP"); + })); + setStatus(st); + if (status != "UP") return; + } client.setID(addr.id); const val = await addr.func.read(client, addr.addr, addr.len); console.log("DATA %O", val); await mqtt.publishAsync(data, val.buffer); -}); +} +const polling = asyncjs.queue(asyncjs.timeout(poll, 10000)); +polling.error(e => console.log("POLL ERR: %o", e)); function setAddrs (pkt) { if (pkt.version != 1) { From 9690b026489c7816bdb1921baff04f74279e00d0 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Fri, 12 Jul 2024 11:12:36 +0100 Subject: [PATCH 17/36] Refactor into a general driver class This class will only handle drivers which can't do their own polling, and ignore the address groups. Drivers like OPCUA will need a different implementation. This driver is also careful to always poll serially. Some drivers might be able to handle parallel polls, and we should allow this. (Modbus can't.) --- edge-modbus/.gitignore | 1 + edge-modbus/bin/driver.js | 158 +-------------------------------- edge-modbus/lib/edge-driver.js | 147 ++++++++++++++++++++++++++++++ edge-modbus/lib/modbus.js | 78 ++++++++++++++++ 4 files changed, 230 insertions(+), 154 deletions(-) create mode 100644 edge-modbus/lib/edge-driver.js create mode 100644 edge-modbus/lib/modbus.js diff --git a/edge-modbus/.gitignore b/edge-modbus/.gitignore index 3c3629e6..6cb772be 100644 --- a/edge-modbus/.gitignore +++ b/edge-modbus/.gitignore @@ -1 +1,2 @@ node_modules +tmp diff --git a/edge-modbus/bin/driver.js b/edge-modbus/bin/driver.js index 1ebe83f1..2ff87dca 100644 --- a/edge-modbus/bin/driver.js +++ b/edge-modbus/bin/driver.js @@ -3,158 +3,8 @@ * Copyright 2024 AMRC */ -import asyncjs from "async"; -import MQTT from "mqtt"; -import ModbusRTU from "modbus-serial"; +import { PolledDriver } from "../lib/edge-driver.js"; +import { modbusHandler } from "../lib/modbus.js"; -const id = process.env.EDGE_USERNAME; - -const topic = (msg, data) => `fpEdge1/${id}/${msg}` + (data ? `/${data}` : ""); - -const mqtt = MQTT.connect({ - url: process.env.EDGE_MQTT, - clientId: id, - username: id, - password: process.env.EDGE_PASSWORD, - will: { - topic: topic("status"), - payload: "DOWN", - }, - manualConnect: true, - resubscribe: false, -}); - -let status = "DOWN"; -function setStatus (st) { - status = st; - if (mqtt.connected) - mqtt.publish(topic("status"), status); -} - -let client; -async function setConf (conf) { - if (conf.protocol != "tcp") - return "CONF"; - - client = new ModbusRTU(); - return client.connectTCP(conf.host, { port: conf.port }) - .then(() => "UP", () => "CONN"); -} - -let addrs; - -const funcs = { - input: { - read: (c, a, l) => c.readInputRegisters(a, l), - }, - holding: { - read: (c, a, l) => c.readHoldingRegisters(a, l), - }, - coil: { - read: (c, a, l) => c.readCoils(a, l), - }, - discrete: { - read: (c, a, l) => c.readDiscreteInputs(a, l), - }, -}; - -function parseAddr (spec) { - const parts = spec.split(","); - if (parts.length != 4) return; - - const id = Number.parseInt(parts[0]); - if (Number.isNaN(id) || id < 0) return; - const func = funcs[parts[1]]; - if (!func) return; - const addr = Number.parseInt(parts[2]); - if (Number.isNaN(addr) || addr < 0) return; - const len = Number.parseInt(parts[3]); - if (Number.isNaN(len) || len < 1) return; - - return { id, func, addr, len }; -} - -async function poll ({ data, addr }) { - console.log("READ %O", addr); - if (!client) { - console.log("Can't poll, no client!"); - return; - } - if (!client.isOpen) { - const st = await new Promise((r, j) => - client.open(e => { - console.log("Modbus open: %s", e); - r(e ? "CONN" : "UP"); - })); - setStatus(st); - if (status != "UP") return; - } - - client.setID(addr.id); - const val = await addr.func.read(client, addr.addr, addr.len); - console.log("DATA %O", val); - - await mqtt.publishAsync(data, val.buffer); -} -const polling = asyncjs.queue(asyncjs.timeout(poll, 10000)); -polling.error(e => console.log("POLL ERR: %o", e)); - -function setAddrs (pkt) { - if (pkt.version != 1) { - console.log("Bad addr config version: %s", pkt.version); - return false; - } - - const parsed = Object.entries(pkt.addrs) - .map(([t, a]) => [t, parseAddr(a)]); - if (parsed.some(([, f]) => !f)) { - console.log("BAD ADDRS: %O", pkt.addrs); - return false; - } - - addrs = new Map(parsed); - console.log("Set addrs: %O", addrs); - return true; -} - -mqtt.on("connect", async () => { - await mqtt.subscribeAsync( - "active conf addr poll" - .split(" ") - .map(t => topic(t))); - setStatus("READY"); -}); -mqtt.on("message", async (t, p) => { - const [, , msg, data] = t.split("/"); - switch (msg) { - case "active": - if (p.toString() == "ONLINE") - setStatus("READY"); - break; - case "conf": - const conf = JSON.parse(p.toString()); - console.log("CONF: %O", conf); - setStatus(await setConf(conf)); - break; - case "addr": - const a = JSON.parse(p.toString()); - if (!setAddrs(a)) - setStatus("CONF"); - break; - case "poll": - const poll = p.toString().split("\n"); - console.log("POLL %s %O", polling.length(), poll); - if (addrs) - poll.map(t => ({ - data: topic("data", t), - addr: addrs.get(t), - })) - .filter(({ addr }) => addr) - .forEach(a => polling.push(a)); - else - console.log("Can't poll, no addrs!"); - break; - } -}); - -mqtt.connect(); +const drv = new PolledDriver(process.env, modbusHandler); +drv.run(); diff --git a/edge-modbus/lib/edge-driver.js b/edge-modbus/lib/edge-driver.js new file mode 100644 index 00000000..7917a98f --- /dev/null +++ b/edge-modbus/lib/edge-driver.js @@ -0,0 +1,147 @@ +/* AMRC Connectivity Stack + * Edge Agent driver library + * Copyright 2024 AMRC + */ + +import asyncjs from "async"; +import MQTT from "mqtt"; + +const Q_TIMEOUT = 30000; +const Q_MAX = 20; + +export class PolledDriver { + constructor (env, createHandler) { + this.createHandler = createHandler; + + this.status = "DOWN"; + this.addrs = null; + + this.id = env.EDGE_USERNAME; + this.mqtt = this.createMqttClient(env.EDGE_MQTT, env.EDGE_PASSWORD); + this.polling = this.createPollQueue(); + } + + run () { + this.mqtt.connect(); + } + + topic (msg, data) { + return `fpEdge1/${this.id}/${msg}` + (data ? `/${data}` : ""); + } + + createMqttClient (broker, password) { + const mqtt = MQTT.connect({ + url: process.env.EDGE_MQTT, + clientId: this.id, + username: this.id, + password: process.env.EDGE_PASSWORD, + will: { + topic: this.topic("status"), + payload: "DOWN", + }, + manualConnect: true, + resubscribe: false, + }); + + mqtt.on("connect", () => this.connected()); + mqtt.on("message", (t, p) => this.handleMessage(t, p)); + + return mqtt; + } + + createPollQueue () { + const poll = this.poll.bind(this); + const q = asyncjs.queue(asyncjs.timeout(poll, Q_TIMEOUT)); + q.error(e => console.log("POLL ERR: %o", e)); + + return q; + } + + setStatus (st) { + this.status = st; + if (this.mqtt.connected) + this.mqtt.publish(this.topic("status"), st); + } + + setAddrs (pkt) { + if (!this.handler) { + console.log("Received addrs without handler"); + return false; + } + + if (pkt.version != 1) { + console.log("Bad addr config version: %s", pkt.version); + return false; + } + + const parsed = Object.entries(pkt.addrs) + .map(([t, a]) => [t, this.handler.parseAddr(a)]); + + if (parsed.some(([, f]) => !f)) { + console.log("BAD ADDRS: %O", pkt.addrs); + return false; + } + + this.addrs = new Map(parsed); + console.log("Set addrs: %O", this.addrs); + return true; + } + + async poll ({ data, spec }) { + console.log("READ %O", spec); + const buf = await this.handler.poll(spec); + + console.log("DATA %O", buf); + if (buf) + await this.mqtt.publishAsync(data, buf); + } + + async connected () { + await this.mqtt.subscribeAsync( + "active conf addr poll" + .split(" ") + .map(t => this.topic(t))); + this.setStatus("READY"); + } + + async handleMessage (topic, p) { + const [, , msg, data] = topic.split("/"); + switch (msg) { + case "active": + if (p.toString() == "ONLINE") + this.setStatus("READY"); + break; + + case "conf": + const conf = JSON.parse(p.toString()); + console.log("CONF: %O", conf); + this.addrs = null; + this.handler = this.createHandler(this, conf); + if (this.handler) + this.handler.run(); + else + this.setStatus("CONF"); + break; + + case "addr": + const a = JSON.parse(p.toString()); + if (!this.setAddrs(a)) + this.setStatus("CONF"); + break; + + case "poll": + const poll = p.toString().split("\n"); + console.log("POLL %s %O", this.polling.length(), poll); + if (this.addrs) + poll.map(t => ({ + data: this.topic("data", t), + spec: this.addrs.get(t), + })) + .filter(v => v.spec) + .forEach(a => this.polling.push(a)); + else + console.log("Can't poll, no addrs!"); + break; + } + } +} diff --git a/edge-modbus/lib/modbus.js b/edge-modbus/lib/modbus.js new file mode 100644 index 00000000..15ca39e5 --- /dev/null +++ b/edge-modbus/lib/modbus.js @@ -0,0 +1,78 @@ +/* AMRC Connectivity Stack + * Modbus Edge Agent driver + * Copyright 2024 AMRC + */ + +import ModbusRTU from "modbus-serial"; + +const funcs = { + input: { + read: (c, a, l) => c.readInputRegisters(a, l), + }, + holding: { + read: (c, a, l) => c.readHoldingRegisters(a, l), + }, + coil: { + read: (c, a, l) => c.readCoils(a, l), + }, + discrete: { + read: (c, a, l) => c.readDiscreteInputs(a, l), + }, +}; + +class ModbusHandler { + constructor (driver, conf) { + this.driver = driver; + this.conf = conf; + this.client = new ModbusRTU(); + } + + run () { + const { client, driver } = this; + const { host, port } = this.conf; + + client.connectTCP(host, { port }) + .then(() => driver.setStatus("UP")) + .catch(() => driver.setStatus("CONN")); + } + + parseAddr (spec) { + const parts = spec.split(","); + if (parts.length != 4) return; + + const id = Number.parseInt(parts[0]); + if (Number.isNaN(id) || id < 0) return; + const func = funcs[parts[1]]; + if (!func) return; + const addr = Number.parseInt(parts[2]); + if (Number.isNaN(addr) || addr < 0) return; + const len = Number.parseInt(parts[3]); + if (Number.isNaN(len) || len < 1) return; + + return { id, func, addr, len }; + } + + async poll (addr) { + const { client } = this; + + if (!client.isOpen) { + const st = await new Promise((r, j) => + client.open(e => { + console.log("Modbus open: %s", e); + r(e ? "CONN" : "UP"); + })); + this.driver.setStatus(st); + if (st != "UP") return; + } + + client.setID(addr.id); + const val = await addr.func.read(client, addr.addr, addr.len); + return val.buffer; + } +} + +export function modbusHandler (driver, conf) { + if (conf.protocol != "tcp") + return; + return new ModbusHandler(driver, conf); +} From bd6b8437c54d10c7f8b2b77f770e967c38e30aeb Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Fri, 12 Jul 2024 12:17:54 +0100 Subject: [PATCH 18/36] Handle reconnection better We want to maintain a continuous connection to the modbus device. We also want to alert the edge agent promptly if we lose our connection. Handle reconnection via connection events rather than waiting for a poll. Delay 5s before attempting to reconnect, otherwise we may enter a fast loop. --- edge-modbus/lib/modbus.js | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/edge-modbus/lib/modbus.js b/edge-modbus/lib/modbus.js index 15ca39e5..aab4d529 100644 --- a/edge-modbus/lib/modbus.js +++ b/edge-modbus/lib/modbus.js @@ -5,6 +5,8 @@ import ModbusRTU from "modbus-serial"; +const RECONNECT = 5000; + const funcs = { input: { read: (c, a, l) => c.readInputRegisters(a, l), @@ -24,16 +26,35 @@ class ModbusHandler { constructor (driver, conf) { this.driver = driver; this.conf = conf; + this.client = new ModbusRTU(); } run () { - const { client, driver } = this; + const { driver, client, sock } = this; const { host, port } = this.conf; + client.on("close", () => this.reconnect()); + client.connectTCP(host, { port }) .then(() => driver.setStatus("UP")) - .catch(() => driver.setStatus("CONN")); + .catch(() => this.reconnect()); + } + + async reconnect () { + const { client, driver } = this; + + console.log("Modbus connection closed"); + setTimeout(() => { + console.log("Reconnecting to modbus"); + client.open(e => { + driver.setStatus(e ? "CONN" : "UP"); + if (e) { + console.log("Failed to connect to modbus: %s", e); + this.reconnect(); + } + }); + }, RECONNECT); } parseAddr (spec) { @@ -55,15 +76,7 @@ class ModbusHandler { async poll (addr) { const { client } = this; - if (!client.isOpen) { - const st = await new Promise((r, j) => - client.open(e => { - console.log("Modbus open: %s", e); - r(e ? "CONN" : "UP"); - })); - this.driver.setStatus(st); - if (st != "UP") return; - } + if (!client.isOpen) return; client.setID(addr.id); const val = await addr.func.read(client, addr.addr, addr.len); From bf69e0e654f0313cade3bc1205ce75448c70919d Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Fri, 12 Jul 2024 13:49:27 +0100 Subject: [PATCH 19/36] Add a Dockerfile These drivers do not need the GSSAPI code, so should not need our customised base images. --- edge-modbus/Dockerfile | 33 +++++++++++++++++++++++++++++++++ edge-modbus/Makefile | 6 ++++++ 2 files changed, 39 insertions(+) create mode 100644 edge-modbus/Dockerfile create mode 100644 edge-modbus/Makefile diff --git a/edge-modbus/Dockerfile b/edge-modbus/Dockerfile new file mode 100644 index 00000000..7163f915 --- /dev/null +++ b/edge-modbus/Dockerfile @@ -0,0 +1,33 @@ +# syntax=docker/dockerfile:1 + +FROM node:22-alpine AS build +ARG acs_npm=NO +ARG revision=unknown + +USER root +RUN <<'SHELL' + install -d -o node -g node /home/node/app +SHELL +WORKDIR /home/node/app +USER node +COPY package*.json ./ +RUN <<'SHELL' + touch /home/node/.npmrc + if [ "${acs_npm}" != NO ] + then + npm config set @amrc-factoryplus:registry "${acs_npm}" + fi + + npm install --save=false +SHELL +COPY --chown=node . . +RUN <<'SHELL' + echo "export const GIT_VERSION=\"$revision\";" > ./lib/git-version.js +SHELL + +FROM node:22-alpine AS run +# Copy across from the build container. +WORKDIR /home/node/app +COPY --from=build --chown=root:root /home/node/app ./ +USER node +CMD node bin/driver.js diff --git a/edge-modbus/Makefile b/edge-modbus/Makefile new file mode 100644 index 00000000..fc81e800 --- /dev/null +++ b/edge-modbus/Makefile @@ -0,0 +1,6 @@ +top=.. +include ${top}/mk/acs.init.mk + +repo?=edge-modbus + +include ${mk}/acs.js.mk From 3ab39b79683ffbec919257d29575cd8b277610ef Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Mon, 15 Jul 2024 12:03:00 +0100 Subject: [PATCH 20/36] Migrate to js-edge-driver library --- edge-modbus/bin/driver.js | 8 +- edge-modbus/lib/edge-driver.js | 147 --------------------------------- edge-modbus/package-lock.json | 28 +++++-- edge-modbus/package.json | 9 +- 4 files changed, 33 insertions(+), 159 deletions(-) delete mode 100644 edge-modbus/lib/edge-driver.js diff --git a/edge-modbus/bin/driver.js b/edge-modbus/bin/driver.js index 2ff87dca..e41eb62c 100644 --- a/edge-modbus/bin/driver.js +++ b/edge-modbus/bin/driver.js @@ -3,8 +3,12 @@ * Copyright 2024 AMRC */ -import { PolledDriver } from "../lib/edge-driver.js"; +import { PolledDriver } from "@amrc-factoryplus/edge-driver"; import { modbusHandler } from "../lib/modbus.js"; -const drv = new PolledDriver(process.env, modbusHandler); +const drv = new PolledDriver({ + env: process.env, + handler: modbusHandler, + serial: true, +}); drv.run(); diff --git a/edge-modbus/lib/edge-driver.js b/edge-modbus/lib/edge-driver.js deleted file mode 100644 index 7917a98f..00000000 --- a/edge-modbus/lib/edge-driver.js +++ /dev/null @@ -1,147 +0,0 @@ -/* AMRC Connectivity Stack - * Edge Agent driver library - * Copyright 2024 AMRC - */ - -import asyncjs from "async"; -import MQTT from "mqtt"; - -const Q_TIMEOUT = 30000; -const Q_MAX = 20; - -export class PolledDriver { - constructor (env, createHandler) { - this.createHandler = createHandler; - - this.status = "DOWN"; - this.addrs = null; - - this.id = env.EDGE_USERNAME; - this.mqtt = this.createMqttClient(env.EDGE_MQTT, env.EDGE_PASSWORD); - this.polling = this.createPollQueue(); - } - - run () { - this.mqtt.connect(); - } - - topic (msg, data) { - return `fpEdge1/${this.id}/${msg}` + (data ? `/${data}` : ""); - } - - createMqttClient (broker, password) { - const mqtt = MQTT.connect({ - url: process.env.EDGE_MQTT, - clientId: this.id, - username: this.id, - password: process.env.EDGE_PASSWORD, - will: { - topic: this.topic("status"), - payload: "DOWN", - }, - manualConnect: true, - resubscribe: false, - }); - - mqtt.on("connect", () => this.connected()); - mqtt.on("message", (t, p) => this.handleMessage(t, p)); - - return mqtt; - } - - createPollQueue () { - const poll = this.poll.bind(this); - const q = asyncjs.queue(asyncjs.timeout(poll, Q_TIMEOUT)); - q.error(e => console.log("POLL ERR: %o", e)); - - return q; - } - - setStatus (st) { - this.status = st; - if (this.mqtt.connected) - this.mqtt.publish(this.topic("status"), st); - } - - setAddrs (pkt) { - if (!this.handler) { - console.log("Received addrs without handler"); - return false; - } - - if (pkt.version != 1) { - console.log("Bad addr config version: %s", pkt.version); - return false; - } - - const parsed = Object.entries(pkt.addrs) - .map(([t, a]) => [t, this.handler.parseAddr(a)]); - - if (parsed.some(([, f]) => !f)) { - console.log("BAD ADDRS: %O", pkt.addrs); - return false; - } - - this.addrs = new Map(parsed); - console.log("Set addrs: %O", this.addrs); - return true; - } - - async poll ({ data, spec }) { - console.log("READ %O", spec); - const buf = await this.handler.poll(spec); - - console.log("DATA %O", buf); - if (buf) - await this.mqtt.publishAsync(data, buf); - } - - async connected () { - await this.mqtt.subscribeAsync( - "active conf addr poll" - .split(" ") - .map(t => this.topic(t))); - this.setStatus("READY"); - } - - async handleMessage (topic, p) { - const [, , msg, data] = topic.split("/"); - switch (msg) { - case "active": - if (p.toString() == "ONLINE") - this.setStatus("READY"); - break; - - case "conf": - const conf = JSON.parse(p.toString()); - console.log("CONF: %O", conf); - this.addrs = null; - this.handler = this.createHandler(this, conf); - if (this.handler) - this.handler.run(); - else - this.setStatus("CONF"); - break; - - case "addr": - const a = JSON.parse(p.toString()); - if (!this.setAddrs(a)) - this.setStatus("CONF"); - break; - - case "poll": - const poll = p.toString().split("\n"); - console.log("POLL %s %O", this.polling.length(), poll); - if (this.addrs) - poll.map(t => ({ - data: this.topic("data", t), - spec: this.addrs.get(t), - })) - .filter(v => v.spec) - .forEach(a => this.polling.push(a)); - else - console.log("Can't poll, no addrs!"); - break; - } - } -} diff --git a/edge-modbus/package-lock.json b/edge-modbus/package-lock.json index a0734d48..d97196bb 100644 --- a/edge-modbus/package-lock.json +++ b/edge-modbus/package-lock.json @@ -1,16 +1,25 @@ { - "name": "amrc-connectivity-stack", - "version": "1.0.0", + "name": "edge-modbus", + "version": "0.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "amrc-connectivity-stack", - "version": "1.0.0", + "name": "edge-modbus", + "version": "0.0.0", + "license": "ISC", + "dependencies": { + "@amrc-factoryplus/edge-driver": "0.0.1-bmz2", + "modbus-serial": "^8.0.17" + } + }, + "node_modules/@amrc-factoryplus/edge-driver": { + "version": "0.0.1-bmz2", + "resolved": "https://npm.amrc-factoryplus-dev.shef.ac.uk/@amrc-factoryplus%2fedge-driver/-/edge-driver-0.0.1-bmz2.tgz", + "integrity": "sha512-ZpMyfNWxtboGRYl/B4oqG1a3VX00Pw0yILTJXTN/1SB9GTsFj5zJ+qkJOGL7G9XMM3QFjV7h9YtZH36wrokJcw==", "license": "ISC", "dependencies": { "async": "^3.2.5", - "modbus-serial": "^8.0.17", "mqtt": "^5.8.0" } }, @@ -751,6 +760,15 @@ } }, "dependencies": { + "@amrc-factoryplus/edge-driver": { + "version": "0.0.1-bmz2", + "resolved": "https://npm.amrc-factoryplus-dev.shef.ac.uk/@amrc-factoryplus%2fedge-driver/-/edge-driver-0.0.1-bmz2.tgz", + "integrity": "sha512-ZpMyfNWxtboGRYl/B4oqG1a3VX00Pw0yILTJXTN/1SB9GTsFj5zJ+qkJOGL7G9XMM3QFjV7h9YtZH36wrokJcw==", + "requires": { + "async": "^3.2.5", + "mqtt": "^5.8.0" + } + }, "@babel/runtime": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", diff --git a/edge-modbus/package.json b/edge-modbus/package.json index 5692f866..379a68e0 100644 --- a/edge-modbus/package.json +++ b/edge-modbus/package.json @@ -1,7 +1,7 @@ { - "name": "amrc-connectivity-stack", - "version": "1.0.0", - "description": "The AMRC Connectivity Stack (ACS) is a comprehensive collection of open-source services developed by the AMRC that represents a complete end-to-end implementation of the [Factory+](https://factoryplus.app.amrc.co.uk) framework. It is distributed as a Kubernetes Helm chart an can be deployed onto any Kubernetes cluster.", + "name": "edge-modbus", + "version": "0.0.0", + "description": "", "main": "index.js", "type": "module", "directories": { @@ -14,8 +14,7 @@ "author": "", "license": "ISC", "dependencies": { - "async": "^3.2.5", "modbus-serial": "^8.0.17", - "mqtt": "^5.8.0" + "@amrc-factoryplus/edge-driver": "0.0.1-bmz2" } } From bf32dbeb0c2c3097fae68277ee5212b8de1f0361 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Mon, 15 Jul 2024 12:19:31 +0100 Subject: [PATCH 21/36] Refactor using @amrc-factoryplus/edge-driver --- edge-test/Dockerfile | 33 +++++++++ edge-test/Makefile | 6 ++ edge-test/bin/driver.js | 131 +++--------------------------------- edge-test/lib/test.js | 58 ++++++++++++++++ edge-test/package-lock.json | 32 +++++++-- edge-test/package.json | 1 + 6 files changed, 132 insertions(+), 129 deletions(-) create mode 100644 edge-test/Dockerfile create mode 100644 edge-test/Makefile create mode 100644 edge-test/lib/test.js diff --git a/edge-test/Dockerfile b/edge-test/Dockerfile new file mode 100644 index 00000000..7163f915 --- /dev/null +++ b/edge-test/Dockerfile @@ -0,0 +1,33 @@ +# syntax=docker/dockerfile:1 + +FROM node:22-alpine AS build +ARG acs_npm=NO +ARG revision=unknown + +USER root +RUN <<'SHELL' + install -d -o node -g node /home/node/app +SHELL +WORKDIR /home/node/app +USER node +COPY package*.json ./ +RUN <<'SHELL' + touch /home/node/.npmrc + if [ "${acs_npm}" != NO ] + then + npm config set @amrc-factoryplus:registry "${acs_npm}" + fi + + npm install --save=false +SHELL +COPY --chown=node . . +RUN <<'SHELL' + echo "export const GIT_VERSION=\"$revision\";" > ./lib/git-version.js +SHELL + +FROM node:22-alpine AS run +# Copy across from the build container. +WORKDIR /home/node/app +COPY --from=build --chown=root:root /home/node/app ./ +USER node +CMD node bin/driver.js diff --git a/edge-test/Makefile b/edge-test/Makefile new file mode 100644 index 00000000..85f1a1ef --- /dev/null +++ b/edge-test/Makefile @@ -0,0 +1,6 @@ +top=.. +include ${top}/mk/acs.init.mk + +repo?=edge-test + +include ${mk}/acs.js.mk diff --git a/edge-test/bin/driver.js b/edge-test/bin/driver.js index c431e1ce..dc93af92 100644 --- a/edge-test/bin/driver.js +++ b/edge-test/bin/driver.js @@ -1,129 +1,14 @@ /* AMRC Connectivity Stack - * Modbus Edge Agent driver + * Testing Edge Agent driver * Copyright 2024 AMRC */ -import asyncjs from "async"; -import MQTT from "mqtt"; +import { PolledDriver } from "@amrc-factoryplus/edge-driver"; +import { handleTest } from "../lib/test.js"; -const id = process.env.EDGE_USERNAME; - -const topic = (msg, data) => `fpEdge1/${id}/${msg}` + (data ? `/${data}` : ""); - -const mqtt = MQTT.connect({ - url: process.env.EDGE_MQTT, - clientId: id, - username: id, - password: process.env.EDGE_PASSWORD, - will: { - topic: topic("status"), - payload: "DOWN", - }, - manualConnect: true, - resubscribe: false, +const drv = new PolledDriver({ + env: process.env, + handler: handleTest, + serial: false, }); - -let status = "DOWN"; -function setStatus (st) { - status = st; - if (mqtt.connected) - mqtt.publish(topic("status"), status); -} -let conf; -let addrs; - -const funcs = { - const: (p, a) => t => a, - sin: (p, a) => t => a * Math.sin(2 * Math.PI * (t / p)), - saw: (p, a) => t => (a / p) * (t % p), -}; -const packing = { - bd: [8, (b, v) => b.writeDoubleBE(v)], - ld: [8, (b, v) => b.writeDoubleLE(v)], - bf: [4, (b, v) => b.writeFloatBE(v)], - lf: [4, (b, v) => b.writeFloatLE(v)], -}; - -function parseAddr (addr) { - const parts = addr.split(":"); - if (parts.length != 4) return; - - const func = funcs[parts[0]]; - if (!func) return; - const period = Number.parseFloat(parts[1]); - if (Number.isNaN(period)) return; - const amplitude = Number.parseFloat(parts[2]); - if (Number.isNaN(amplitude)) return; - const pack = packing[parts[3]]; - if (!pack) return; - - return [func(period, amplitude), ...pack]; -} - -function setAddrs (pkt) { - if (pkt.version != 1) { - console.log("Bad addr config version: %s", pkt.version); - return false; - } - - const parsed = Object.entries(pkt.addrs) - .map(([t, a]) => [t, parseAddr(a)]); - if (parsed.some(([, f]) => !f)) { - console.log("BAD ADDRS: %O", pkt.addrs); - return false; - } - - addrs = new Map( - parsed.map(([t, [f, s, p]]) => - [t, { topic: t, func: f, size: s, pack: p }])); - console.log("Set addrs: %O", addrs); - return true; -} - -const polling = asyncjs.queue(async addr => { - const val = addr.func(performance.now()); - const buf = Buffer.alloc(addr.size); - const top = topic("data", addr.topic); - - addr.pack(buf, val); - await mqtt.publishAsync(top, buf); -}); - -mqtt.on("connect", async () => { - await mqtt.subscribeAsync( - "active conf addr poll" - .split(" ") - .map(t => topic(t))); - setStatus("READY"); -}); -mqtt.on("message", (t, p) => { - const [, , msg, data] = t.split("/"); - switch (msg) { - case "active": - if (p.toString() == "ONLINE") - setStatus("READY"); - break; - case "conf": - conf = JSON.parse(p.toString()); - console.log("CONF: %O", conf); - setStatus("UP"); - break; - case "addr": - const a = JSON.parse(p.toString()); - if (!setAddrs(a)) - setStatus("CONF"); - break; - case "poll": - const poll = p.toString().split("\n"); - console.log("POLL %O", poll); - if (addrs) - poll.map(t => addrs.get(t)) - .filter(a => a) - .forEach(a => polling.push(a)); - else - console.log("Can't poll, no addrs!"); - break; - } -}); - -mqtt.connect(); +drv.run(); diff --git a/edge-test/lib/test.js b/edge-test/lib/test.js new file mode 100644 index 00000000..1cd529dd --- /dev/null +++ b/edge-test/lib/test.js @@ -0,0 +1,58 @@ +/* + * Edge Agent testing driver + * Copyright 2024 AMRC + */ + +const funcs = { + const: (p, a) => t => a, + sin: (p, a) => t => a * Math.sin(2 * Math.PI * (t / p)), + saw: (p, a) => t => (a / p) * (t % p), +}; +const packing = { + bd: [8, (b, v) => b.writeDoubleBE(v)], + ld: [8, (b, v) => b.writeDoubleLE(v)], + bf: [4, (b, v) => b.writeFloatBE(v)], + lf: [4, (b, v) => b.writeFloatLE(v)], +}; + +class TestHandler { + constructor (driver) { + this.driver = driver; + } + + run () { + this.driver.setStatus("UP"); + } + + parseAddr (spec) { + const parts = spec.split(":"); + if (parts.length != 4) return; + + const func = funcs[parts[0]]; + if (!func) return; + const period = Number.parseFloat(parts[1]); + if (Number.isNaN(period)) return; + const amplitude = Number.parseFloat(parts[2]); + if (Number.isNaN(amplitude)) return; + const pack = packing[parts[3]]; + if (!pack) return; + + return { + func: func(period, amplitude), + size: pack[0], + pack: pack[1], + }; + } + + async poll (addr) { + const val = addr.func(performance.now()); + const buf = Buffer.alloc(addr.size); + addr.pack(buf, val); + + return buf; + } +} + +export function handleTest (driver, conf) { + return new TestHandler(driver); +} diff --git a/edge-test/package-lock.json b/edge-test/package-lock.json index b553adfa..7d971948 100644 --- a/edge-test/package-lock.json +++ b/edge-test/package-lock.json @@ -9,10 +9,21 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@amrc-factoryplus/edge-driver": "^0.0.1-bmz2", "async": "^3.2.5", "mqtt": "^5.7.2" } }, + "node_modules/@amrc-factoryplus/edge-driver": { + "version": "0.0.1-bmz2", + "resolved": "https://npm.amrc-factoryplus-dev.shef.ac.uk/@amrc-factoryplus%2fedge-driver/-/edge-driver-0.0.1-bmz2.tgz", + "integrity": "sha512-ZpMyfNWxtboGRYl/B4oqG1a3VX00Pw0yILTJXTN/1SB9GTsFj5zJ+qkJOGL7G9XMM3QFjV7h9YtZH36wrokJcw==", + "license": "ISC", + "dependencies": { + "async": "^3.2.5", + "mqtt": "^5.8.0" + } + }, "node_modules/@babel/runtime": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", @@ -254,9 +265,9 @@ } }, "node_modules/mqtt": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.7.2.tgz", - "integrity": "sha512-b5xIA9J/K1LTubSWKaNYYLxYIusQdip6o9/8bRWad2TelRr8xLifjQt+SnamDAwMp3O6NdvR9E8ae7VMuN02kg==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.8.0.tgz", + "integrity": "sha512-/+H04mv6goy6K5gHMNH3uS0icBzXapS+4uUf4yZyQWXi72APPZNb81bQhvkm99poEQettXVT8XETB0mPxl5Wjg==", "dependencies": { "@types/readable-stream": "^4.0.5", "@types/ws": "^8.5.9", @@ -465,6 +476,15 @@ } }, "dependencies": { + "@amrc-factoryplus/edge-driver": { + "version": "0.0.1-bmz2", + "resolved": "https://npm.amrc-factoryplus-dev.shef.ac.uk/@amrc-factoryplus%2fedge-driver/-/edge-driver-0.0.1-bmz2.tgz", + "integrity": "sha512-ZpMyfNWxtboGRYl/B4oqG1a3VX00Pw0yILTJXTN/1SB9GTsFj5zJ+qkJOGL7G9XMM3QFjV7h9YtZH36wrokJcw==", + "requires": { + "async": "^3.2.5", + "mqtt": "^5.8.0" + } + }, "@babel/runtime": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", @@ -627,9 +647,9 @@ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" }, "mqtt": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.7.2.tgz", - "integrity": "sha512-b5xIA9J/K1LTubSWKaNYYLxYIusQdip6o9/8bRWad2TelRr8xLifjQt+SnamDAwMp3O6NdvR9E8ae7VMuN02kg==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.8.0.tgz", + "integrity": "sha512-/+H04mv6goy6K5gHMNH3uS0icBzXapS+4uUf4yZyQWXi72APPZNb81bQhvkm99poEQettXVT8XETB0mPxl5Wjg==", "requires": { "@types/readable-stream": "^4.0.5", "@types/ws": "^8.5.9", diff --git a/edge-test/package.json b/edge-test/package.json index c03bd6db..b28f7929 100644 --- a/edge-test/package.json +++ b/edge-test/package.json @@ -11,6 +11,7 @@ "author": "", "license": "ISC", "dependencies": { + "@amrc-factoryplus/edge-driver": "^0.0.1-bmz2", "async": "^3.2.5", "mqtt": "^5.7.2" } From 7682bf135bdb95fa1a25a48b96e16b9af123b97d Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Tue, 16 Jul 2024 11:48:06 +0100 Subject: [PATCH 22/36] Update edge drivers to the new API --- edge-modbus/lib/modbus.js | 18 ++++++++++++++---- edge-modbus/package-lock.json | 14 +++++++------- edge-modbus/package.json | 2 +- edge-test/lib/test.js | 12 +++--------- edge-test/package-lock.json | 14 +++++++------- edge-test/package.json | 2 +- 6 files changed, 33 insertions(+), 29 deletions(-) diff --git a/edge-modbus/lib/modbus.js b/edge-modbus/lib/modbus.js index aab4d529..7122d2af 100644 --- a/edge-modbus/lib/modbus.js +++ b/edge-modbus/lib/modbus.js @@ -28,17 +28,27 @@ class ModbusHandler { this.conf = conf; this.client = new ModbusRTU(); + this.on_close = () => this.reconnect(); } run () { - const { driver, client, sock } = this; + const { driver, client } = this; const { host, port } = this.conf; - client.on("close", () => this.reconnect()); + client.on("close", this.on_close); client.connectTCP(host, { port }) .then(() => driver.setStatus("UP")) - .catch(() => this.reconnect()); + .catch(this.on_close); + + return this; + } + + close () { + const { client } = this; + + client.off("close", this.on_close); + return new Promise(r => client.close(r)); } async reconnect () { @@ -87,5 +97,5 @@ class ModbusHandler { export function modbusHandler (driver, conf) { if (conf.protocol != "tcp") return; - return new ModbusHandler(driver, conf); + return new ModbusHandler(driver, conf).run(); } diff --git a/edge-modbus/package-lock.json b/edge-modbus/package-lock.json index d97196bb..252ecbf4 100644 --- a/edge-modbus/package-lock.json +++ b/edge-modbus/package-lock.json @@ -9,14 +9,14 @@ "version": "0.0.0", "license": "ISC", "dependencies": { - "@amrc-factoryplus/edge-driver": "0.0.1-bmz2", + "@amrc-factoryplus/edge-driver": "0.0.1-bmz3", "modbus-serial": "^8.0.17" } }, "node_modules/@amrc-factoryplus/edge-driver": { - "version": "0.0.1-bmz2", - "resolved": "https://npm.amrc-factoryplus-dev.shef.ac.uk/@amrc-factoryplus%2fedge-driver/-/edge-driver-0.0.1-bmz2.tgz", - "integrity": "sha512-ZpMyfNWxtboGRYl/B4oqG1a3VX00Pw0yILTJXTN/1SB9GTsFj5zJ+qkJOGL7G9XMM3QFjV7h9YtZH36wrokJcw==", + "version": "0.0.1-bmz3", + "resolved": "https://npm.amrc-factoryplus-dev.shef.ac.uk/@amrc-factoryplus%2fedge-driver/-/edge-driver-0.0.1-bmz3.tgz", + "integrity": "sha512-ScEmjMZ8vUH+prjCqEYCfVGbQdA+e7o2lvNZI9pJRah3ULNDthymTmOAB6CEnhd1jwlkdcgmLmbR9GPBok+yGg==", "license": "ISC", "dependencies": { "async": "^3.2.5", @@ -761,9 +761,9 @@ }, "dependencies": { "@amrc-factoryplus/edge-driver": { - "version": "0.0.1-bmz2", - "resolved": "https://npm.amrc-factoryplus-dev.shef.ac.uk/@amrc-factoryplus%2fedge-driver/-/edge-driver-0.0.1-bmz2.tgz", - "integrity": "sha512-ZpMyfNWxtboGRYl/B4oqG1a3VX00Pw0yILTJXTN/1SB9GTsFj5zJ+qkJOGL7G9XMM3QFjV7h9YtZH36wrokJcw==", + "version": "0.0.1-bmz3", + "resolved": "https://npm.amrc-factoryplus-dev.shef.ac.uk/@amrc-factoryplus%2fedge-driver/-/edge-driver-0.0.1-bmz3.tgz", + "integrity": "sha512-ScEmjMZ8vUH+prjCqEYCfVGbQdA+e7o2lvNZI9pJRah3ULNDthymTmOAB6CEnhd1jwlkdcgmLmbR9GPBok+yGg==", "requires": { "async": "^3.2.5", "mqtt": "^5.8.0" diff --git a/edge-modbus/package.json b/edge-modbus/package.json index 379a68e0..5fd9bd8b 100644 --- a/edge-modbus/package.json +++ b/edge-modbus/package.json @@ -15,6 +15,6 @@ "license": "ISC", "dependencies": { "modbus-serial": "^8.0.17", - "@amrc-factoryplus/edge-driver": "0.0.1-bmz2" + "@amrc-factoryplus/edge-driver": "0.0.1-bmz3" } } diff --git a/edge-test/lib/test.js b/edge-test/lib/test.js index 1cd529dd..7081a332 100644 --- a/edge-test/lib/test.js +++ b/edge-test/lib/test.js @@ -16,14 +16,6 @@ const packing = { }; class TestHandler { - constructor (driver) { - this.driver = driver; - } - - run () { - this.driver.setStatus("UP"); - } - parseAddr (spec) { const parts = spec.split(":"); if (parts.length != 4) return; @@ -54,5 +46,7 @@ class TestHandler { } export function handleTest (driver, conf) { - return new TestHandler(driver); + const h = new TestHandler(); + driver.setStatus("UP"); + return h; } diff --git a/edge-test/package-lock.json b/edge-test/package-lock.json index 7d971948..12d73b87 100644 --- a/edge-test/package-lock.json +++ b/edge-test/package-lock.json @@ -9,15 +9,15 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@amrc-factoryplus/edge-driver": "^0.0.1-bmz2", + "@amrc-factoryplus/edge-driver": "^0.0.1-bmz3", "async": "^3.2.5", "mqtt": "^5.7.2" } }, "node_modules/@amrc-factoryplus/edge-driver": { - "version": "0.0.1-bmz2", - "resolved": "https://npm.amrc-factoryplus-dev.shef.ac.uk/@amrc-factoryplus%2fedge-driver/-/edge-driver-0.0.1-bmz2.tgz", - "integrity": "sha512-ZpMyfNWxtboGRYl/B4oqG1a3VX00Pw0yILTJXTN/1SB9GTsFj5zJ+qkJOGL7G9XMM3QFjV7h9YtZH36wrokJcw==", + "version": "0.0.1-bmz3", + "resolved": "https://npm.amrc-factoryplus-dev.shef.ac.uk/@amrc-factoryplus%2fedge-driver/-/edge-driver-0.0.1-bmz3.tgz", + "integrity": "sha512-ScEmjMZ8vUH+prjCqEYCfVGbQdA+e7o2lvNZI9pJRah3ULNDthymTmOAB6CEnhd1jwlkdcgmLmbR9GPBok+yGg==", "license": "ISC", "dependencies": { "async": "^3.2.5", @@ -477,9 +477,9 @@ }, "dependencies": { "@amrc-factoryplus/edge-driver": { - "version": "0.0.1-bmz2", - "resolved": "https://npm.amrc-factoryplus-dev.shef.ac.uk/@amrc-factoryplus%2fedge-driver/-/edge-driver-0.0.1-bmz2.tgz", - "integrity": "sha512-ZpMyfNWxtboGRYl/B4oqG1a3VX00Pw0yILTJXTN/1SB9GTsFj5zJ+qkJOGL7G9XMM3QFjV7h9YtZH36wrokJcw==", + "version": "0.0.1-bmz3", + "resolved": "https://npm.amrc-factoryplus-dev.shef.ac.uk/@amrc-factoryplus%2fedge-driver/-/edge-driver-0.0.1-bmz3.tgz", + "integrity": "sha512-ScEmjMZ8vUH+prjCqEYCfVGbQdA+e7o2lvNZI9pJRah3ULNDthymTmOAB6CEnhd1jwlkdcgmLmbR9GPBok+yGg==", "requires": { "async": "^3.2.5", "mqtt": "^5.8.0" diff --git a/edge-test/package.json b/edge-test/package.json index b28f7929..b78218df 100644 --- a/edge-test/package.json +++ b/edge-test/package.json @@ -11,7 +11,7 @@ "author": "", "license": "ISC", "dependencies": { - "@amrc-factoryplus/edge-driver": "^0.0.1-bmz2", + "@amrc-factoryplus/edge-driver": "^0.0.1-bmz3", "async": "^3.2.5", "mqtt": "^5.7.2" } From b68700e501db2e0b3ea5a23982e7e721ee6881a2 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Tue, 16 Jul 2024 14:32:56 +0100 Subject: [PATCH 23/36] Use the Debug object from the driver class --- edge-modbus/lib/modbus.js | 8 +++++--- edge-modbus/package-lock.json | 14 +++++++------- edge-modbus/package.json | 2 +- edge-test/package-lock.json | 14 +++++++------- edge-test/package.json | 2 +- 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/edge-modbus/lib/modbus.js b/edge-modbus/lib/modbus.js index 7122d2af..3ea4cd40 100644 --- a/edge-modbus/lib/modbus.js +++ b/edge-modbus/lib/modbus.js @@ -27,6 +27,8 @@ class ModbusHandler { this.driver = driver; this.conf = conf; + this.log = driver.debug.bound("modbus"); + this.client = new ModbusRTU(); this.on_close = () => this.reconnect(); } @@ -54,13 +56,13 @@ class ModbusHandler { async reconnect () { const { client, driver } = this; - console.log("Modbus connection closed"); + this.log("Modbus connection closed"); setTimeout(() => { - console.log("Reconnecting to modbus"); + this.log("Reconnecting to modbus"); client.open(e => { driver.setStatus(e ? "CONN" : "UP"); if (e) { - console.log("Failed to connect to modbus: %s", e); + this.log("Failed to connect to modbus: %s", e); this.reconnect(); } }); diff --git a/edge-modbus/package-lock.json b/edge-modbus/package-lock.json index 252ecbf4..eced3f47 100644 --- a/edge-modbus/package-lock.json +++ b/edge-modbus/package-lock.json @@ -9,14 +9,14 @@ "version": "0.0.0", "license": "ISC", "dependencies": { - "@amrc-factoryplus/edge-driver": "0.0.1-bmz3", + "@amrc-factoryplus/edge-driver": "0.0.1-bmz4", "modbus-serial": "^8.0.17" } }, "node_modules/@amrc-factoryplus/edge-driver": { - "version": "0.0.1-bmz3", - "resolved": "https://npm.amrc-factoryplus-dev.shef.ac.uk/@amrc-factoryplus%2fedge-driver/-/edge-driver-0.0.1-bmz3.tgz", - "integrity": "sha512-ScEmjMZ8vUH+prjCqEYCfVGbQdA+e7o2lvNZI9pJRah3ULNDthymTmOAB6CEnhd1jwlkdcgmLmbR9GPBok+yGg==", + "version": "0.0.1-bmz4", + "resolved": "https://npm.amrc-factoryplus-dev.shef.ac.uk/@amrc-factoryplus%2fedge-driver/-/edge-driver-0.0.1-bmz4.tgz", + "integrity": "sha512-niiX/PpbliUvVlICRKhly/F6irYu5UxbDN6qxAeJZbJyqVMauTc3z41eO66fNLStppgwxWvgBn0sWkFjhGn8TQ==", "license": "ISC", "dependencies": { "async": "^3.2.5", @@ -761,9 +761,9 @@ }, "dependencies": { "@amrc-factoryplus/edge-driver": { - "version": "0.0.1-bmz3", - "resolved": "https://npm.amrc-factoryplus-dev.shef.ac.uk/@amrc-factoryplus%2fedge-driver/-/edge-driver-0.0.1-bmz3.tgz", - "integrity": "sha512-ScEmjMZ8vUH+prjCqEYCfVGbQdA+e7o2lvNZI9pJRah3ULNDthymTmOAB6CEnhd1jwlkdcgmLmbR9GPBok+yGg==", + "version": "0.0.1-bmz4", + "resolved": "https://npm.amrc-factoryplus-dev.shef.ac.uk/@amrc-factoryplus%2fedge-driver/-/edge-driver-0.0.1-bmz4.tgz", + "integrity": "sha512-niiX/PpbliUvVlICRKhly/F6irYu5UxbDN6qxAeJZbJyqVMauTc3z41eO66fNLStppgwxWvgBn0sWkFjhGn8TQ==", "requires": { "async": "^3.2.5", "mqtt": "^5.8.0" diff --git a/edge-modbus/package.json b/edge-modbus/package.json index 5fd9bd8b..c9be45ce 100644 --- a/edge-modbus/package.json +++ b/edge-modbus/package.json @@ -15,6 +15,6 @@ "license": "ISC", "dependencies": { "modbus-serial": "^8.0.17", - "@amrc-factoryplus/edge-driver": "0.0.1-bmz3" + "@amrc-factoryplus/edge-driver": "0.0.1-bmz4" } } diff --git a/edge-test/package-lock.json b/edge-test/package-lock.json index 12d73b87..b8d518a6 100644 --- a/edge-test/package-lock.json +++ b/edge-test/package-lock.json @@ -9,15 +9,15 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@amrc-factoryplus/edge-driver": "^0.0.1-bmz3", + "@amrc-factoryplus/edge-driver": "^0.0.1-bmz4", "async": "^3.2.5", "mqtt": "^5.7.2" } }, "node_modules/@amrc-factoryplus/edge-driver": { - "version": "0.0.1-bmz3", - "resolved": "https://npm.amrc-factoryplus-dev.shef.ac.uk/@amrc-factoryplus%2fedge-driver/-/edge-driver-0.0.1-bmz3.tgz", - "integrity": "sha512-ScEmjMZ8vUH+prjCqEYCfVGbQdA+e7o2lvNZI9pJRah3ULNDthymTmOAB6CEnhd1jwlkdcgmLmbR9GPBok+yGg==", + "version": "0.0.1-bmz4", + "resolved": "https://npm.amrc-factoryplus-dev.shef.ac.uk/@amrc-factoryplus%2fedge-driver/-/edge-driver-0.0.1-bmz4.tgz", + "integrity": "sha512-niiX/PpbliUvVlICRKhly/F6irYu5UxbDN6qxAeJZbJyqVMauTc3z41eO66fNLStppgwxWvgBn0sWkFjhGn8TQ==", "license": "ISC", "dependencies": { "async": "^3.2.5", @@ -477,9 +477,9 @@ }, "dependencies": { "@amrc-factoryplus/edge-driver": { - "version": "0.0.1-bmz3", - "resolved": "https://npm.amrc-factoryplus-dev.shef.ac.uk/@amrc-factoryplus%2fedge-driver/-/edge-driver-0.0.1-bmz3.tgz", - "integrity": "sha512-ScEmjMZ8vUH+prjCqEYCfVGbQdA+e7o2lvNZI9pJRah3ULNDthymTmOAB6CEnhd1jwlkdcgmLmbR9GPBok+yGg==", + "version": "0.0.1-bmz4", + "resolved": "https://npm.amrc-factoryplus-dev.shef.ac.uk/@amrc-factoryplus%2fedge-driver/-/edge-driver-0.0.1-bmz4.tgz", + "integrity": "sha512-niiX/PpbliUvVlICRKhly/F6irYu5UxbDN6qxAeJZbJyqVMauTc3z41eO66fNLStppgwxWvgBn0sWkFjhGn8TQ==", "requires": { "async": "^3.2.5", "mqtt": "^5.8.0" diff --git a/edge-test/package.json b/edge-test/package.json index b78218df..9af286c0 100644 --- a/edge-test/package.json +++ b/edge-test/package.json @@ -11,7 +11,7 @@ "author": "", "license": "ISC", "dependencies": { - "@amrc-factoryplus/edge-driver": "^0.0.1-bmz3", + "@amrc-factoryplus/edge-driver": "^0.0.1-bmz4", "async": "^3.2.5", "mqtt": "^5.7.2" } From 6821f87796a4ceaddac159b5a55d2390576f441c Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Wed, 17 Jul 2024 10:37:06 +0100 Subject: [PATCH 24/36] Add an update target --- mk/acs.js.mk | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/mk/acs.js.mk b/mk/acs.js.mk index f7c257dd..c715e92f 100644 --- a/mk/acs.js.mk +++ b/mk/acs.js.mk @@ -11,7 +11,7 @@ ifdef acs_npm build_args+= --build-arg acs_npm="${acs_npm}" endif -.PHONY: lint +.PHONY: lint update build: lint @@ -25,6 +25,14 @@ js.eslint: npx eslint ${eslint} endif +update: js.update + @: + +js.update: git.check-committed + npm update + git add . + git commit -m "npm update $$(git rev-parse --show-prefix)" + include ${mk}/acs.docker.mk endif From cbfcd5551abfc0459ad5215c2355297d721bafbe Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Tue, 16 Jul 2024 14:45:14 +0100 Subject: [PATCH 25/36] npm update --- edge-modbus/package-lock.json | 51 +++++++++-------- edge-modbus/package.json | 2 +- edge-test/package-lock.json | 102 ++++++++++++++++------------------ edge-test/package.json | 2 +- 4 files changed, 76 insertions(+), 81 deletions(-) diff --git a/edge-modbus/package-lock.json b/edge-modbus/package-lock.json index eced3f47..287249c4 100644 --- a/edge-modbus/package-lock.json +++ b/edge-modbus/package-lock.json @@ -9,24 +9,23 @@ "version": "0.0.0", "license": "ISC", "dependencies": { - "@amrc-factoryplus/edge-driver": "0.0.1-bmz4", + "@amrc-factoryplus/edge-driver": "^0.0.2", "modbus-serial": "^8.0.17" } }, "node_modules/@amrc-factoryplus/edge-driver": { - "version": "0.0.1-bmz4", - "resolved": "https://npm.amrc-factoryplus-dev.shef.ac.uk/@amrc-factoryplus%2fedge-driver/-/edge-driver-0.0.1-bmz4.tgz", - "integrity": "sha512-niiX/PpbliUvVlICRKhly/F6irYu5UxbDN6qxAeJZbJyqVMauTc3z41eO66fNLStppgwxWvgBn0sWkFjhGn8TQ==", - "license": "ISC", + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@amrc-factoryplus/edge-driver/-/edge-driver-0.0.2.tgz", + "integrity": "sha512-Uhcou4R1IRusqKRO7ae8gwkiTv0UfcTbissi9RN74+C3O9NqHPPYOScGCRjmYwDyKhJUOHsVkoB1BNcfnsLzpg==", "dependencies": { "async": "^3.2.5", "mqtt": "^5.8.0" } }, "node_modules/@babel/runtime": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", - "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.8.tgz", + "integrity": "sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -256,9 +255,9 @@ } }, "node_modules/@types/node": { - "version": "20.14.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", - "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", + "version": "20.14.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz", + "integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==", "dependencies": { "undici-types": "~5.26.4" } @@ -273,9 +272,9 @@ } }, "node_modules/@types/ws": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", - "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "version": "8.5.11", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.11.tgz", + "integrity": "sha512-4+q7P5h3SpJxaBft0Dzpbr6lmMaqh0Jr2tbhJZ/luAwvD7ohSCniYkwz/pLxuT2h0EOa6QADgJj1Ko+TzRfZ+w==", "dependencies": { "@types/node": "*" } @@ -761,18 +760,18 @@ }, "dependencies": { "@amrc-factoryplus/edge-driver": { - "version": "0.0.1-bmz4", - "resolved": "https://npm.amrc-factoryplus-dev.shef.ac.uk/@amrc-factoryplus%2fedge-driver/-/edge-driver-0.0.1-bmz4.tgz", - "integrity": "sha512-niiX/PpbliUvVlICRKhly/F6irYu5UxbDN6qxAeJZbJyqVMauTc3z41eO66fNLStppgwxWvgBn0sWkFjhGn8TQ==", + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@amrc-factoryplus/edge-driver/-/edge-driver-0.0.2.tgz", + "integrity": "sha512-Uhcou4R1IRusqKRO7ae8gwkiTv0UfcTbissi9RN74+C3O9NqHPPYOScGCRjmYwDyKhJUOHsVkoB1BNcfnsLzpg==", "requires": { "async": "^3.2.5", "mqtt": "^5.8.0" } }, "@babel/runtime": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", - "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.8.tgz", + "integrity": "sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==", "requires": { "regenerator-runtime": "^0.14.0" } @@ -899,9 +898,9 @@ } }, "@types/node": { - "version": "20.14.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", - "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", + "version": "20.14.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz", + "integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==", "requires": { "undici-types": "~5.26.4" } @@ -916,9 +915,9 @@ } }, "@types/ws": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", - "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "version": "8.5.11", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.11.tgz", + "integrity": "sha512-4+q7P5h3SpJxaBft0Dzpbr6lmMaqh0Jr2tbhJZ/luAwvD7ohSCniYkwz/pLxuT2h0EOa6QADgJj1Ko+TzRfZ+w==", "requires": { "@types/node": "*" } diff --git a/edge-modbus/package.json b/edge-modbus/package.json index c9be45ce..1dccdd9a 100644 --- a/edge-modbus/package.json +++ b/edge-modbus/package.json @@ -15,6 +15,6 @@ "license": "ISC", "dependencies": { "modbus-serial": "^8.0.17", - "@amrc-factoryplus/edge-driver": "0.0.1-bmz4" + "@amrc-factoryplus/edge-driver": "^0.0.2" } } diff --git a/edge-test/package-lock.json b/edge-test/package-lock.json index b8d518a6..d67012a5 100644 --- a/edge-test/package-lock.json +++ b/edge-test/package-lock.json @@ -9,25 +9,24 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@amrc-factoryplus/edge-driver": "^0.0.1-bmz4", + "@amrc-factoryplus/edge-driver": "^0.0.2", "async": "^3.2.5", "mqtt": "^5.7.2" } }, "node_modules/@amrc-factoryplus/edge-driver": { - "version": "0.0.1-bmz4", - "resolved": "https://npm.amrc-factoryplus-dev.shef.ac.uk/@amrc-factoryplus%2fedge-driver/-/edge-driver-0.0.1-bmz4.tgz", - "integrity": "sha512-niiX/PpbliUvVlICRKhly/F6irYu5UxbDN6qxAeJZbJyqVMauTc3z41eO66fNLStppgwxWvgBn0sWkFjhGn8TQ==", - "license": "ISC", + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@amrc-factoryplus/edge-driver/-/edge-driver-0.0.2.tgz", + "integrity": "sha512-Uhcou4R1IRusqKRO7ae8gwkiTv0UfcTbissi9RN74+C3O9NqHPPYOScGCRjmYwDyKhJUOHsVkoB1BNcfnsLzpg==", "dependencies": { "async": "^3.2.5", "mqtt": "^5.8.0" } }, "node_modules/@babel/runtime": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", - "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.8.tgz", + "integrity": "sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -36,26 +35,26 @@ } }, "node_modules/@types/node": { - "version": "20.14.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.6.tgz", - "integrity": "sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw==", + "version": "20.14.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz", + "integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==", "dependencies": { "undici-types": "~5.26.4" } }, "node_modules/@types/readable-stream": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.14.tgz", - "integrity": "sha512-xZn/AuUbCMShGsqH/ehZtGDwQtbx00M9rZ2ENLe4tOjFZ/JFeWMhEZkk2fEe1jAUqqEAURIkFJ7Az/go8mM1/w==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.15.tgz", + "integrity": "sha512-oAZ3kw+kJFkEqyh7xORZOku1YAKvsFTogRY8kVl4vHpEKiDkfnSA/My8haRE7fvmix5Zyy+1pwzOi7yycGLBJw==", "dependencies": { "@types/node": "*", "safe-buffer": "~5.1.1" } }, "node_modules/@types/ws": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", - "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "version": "8.5.11", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.11.tgz", + "integrity": "sha512-4+q7P5h3SpJxaBft0Dzpbr6lmMaqh0Jr2tbhJZ/luAwvD7ohSCniYkwz/pLxuT2h0EOa6QADgJj1Ko+TzRfZ+w==", "dependencies": { "@types/node": "*" } @@ -96,9 +95,9 @@ ] }, "node_modules/bl": { - "version": "6.0.12", - "resolved": "https://registry.npmjs.org/bl/-/bl-6.0.12.tgz", - "integrity": "sha512-EnEYHilP93oaOa2MnmNEjAcovPS3JlQZOyzGXi3EyEpPhm9qWvdDp7BmAVEVusGzp8LlwQK56Av+OkDoRjzE0w==", + "version": "6.0.14", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.0.14.tgz", + "integrity": "sha512-TJfbvGdL7KFGxTsEbsED7avqpFdY56q9IW0/aiytyheJzxST/+Io6cx/4Qx0K2/u0BPRDs65mjaQzYvMZeNocQ==", "dependencies": { "@types/readable-stream": "^4.0.0", "buffer": "^6.0.3", @@ -249,12 +248,9 @@ } }, "node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", - "engines": { - "node": "14 || >=16.14" - } + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" }, "node_modules/minimist": { "version": "1.2.8", @@ -455,9 +451,9 @@ } }, "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "engines": { "node": ">=10.0.0" }, @@ -477,43 +473,43 @@ }, "dependencies": { "@amrc-factoryplus/edge-driver": { - "version": "0.0.1-bmz4", - "resolved": "https://npm.amrc-factoryplus-dev.shef.ac.uk/@amrc-factoryplus%2fedge-driver/-/edge-driver-0.0.1-bmz4.tgz", - "integrity": "sha512-niiX/PpbliUvVlICRKhly/F6irYu5UxbDN6qxAeJZbJyqVMauTc3z41eO66fNLStppgwxWvgBn0sWkFjhGn8TQ==", + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@amrc-factoryplus/edge-driver/-/edge-driver-0.0.2.tgz", + "integrity": "sha512-Uhcou4R1IRusqKRO7ae8gwkiTv0UfcTbissi9RN74+C3O9NqHPPYOScGCRjmYwDyKhJUOHsVkoB1BNcfnsLzpg==", "requires": { "async": "^3.2.5", "mqtt": "^5.8.0" } }, "@babel/runtime": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", - "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.8.tgz", + "integrity": "sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==", "requires": { "regenerator-runtime": "^0.14.0" } }, "@types/node": { - "version": "20.14.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.6.tgz", - "integrity": "sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw==", + "version": "20.14.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz", + "integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==", "requires": { "undici-types": "~5.26.4" } }, "@types/readable-stream": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.14.tgz", - "integrity": "sha512-xZn/AuUbCMShGsqH/ehZtGDwQtbx00M9rZ2ENLe4tOjFZ/JFeWMhEZkk2fEe1jAUqqEAURIkFJ7Az/go8mM1/w==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.15.tgz", + "integrity": "sha512-oAZ3kw+kJFkEqyh7xORZOku1YAKvsFTogRY8kVl4vHpEKiDkfnSA/My8haRE7fvmix5Zyy+1pwzOi7yycGLBJw==", "requires": { "@types/node": "*", "safe-buffer": "~5.1.1" } }, "@types/ws": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", - "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "version": "8.5.11", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.11.tgz", + "integrity": "sha512-4+q7P5h3SpJxaBft0Dzpbr6lmMaqh0Jr2tbhJZ/luAwvD7ohSCniYkwz/pLxuT2h0EOa6QADgJj1Ko+TzRfZ+w==", "requires": { "@types/node": "*" } @@ -537,9 +533,9 @@ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, "bl": { - "version": "6.0.12", - "resolved": "https://registry.npmjs.org/bl/-/bl-6.0.12.tgz", - "integrity": "sha512-EnEYHilP93oaOa2MnmNEjAcovPS3JlQZOyzGXi3EyEpPhm9qWvdDp7BmAVEVusGzp8LlwQK56Av+OkDoRjzE0w==", + "version": "6.0.14", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.0.14.tgz", + "integrity": "sha512-TJfbvGdL7KFGxTsEbsED7avqpFdY56q9IW0/aiytyheJzxST/+Io6cx/4Qx0K2/u0BPRDs65mjaQzYvMZeNocQ==", "requires": { "@types/readable-stream": "^4.0.0", "buffer": "^6.0.3", @@ -637,9 +633,9 @@ "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==" }, "lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==" + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" }, "minimist": { "version": "1.2.8", @@ -808,9 +804,9 @@ } }, "ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "requires": {} } } diff --git a/edge-test/package.json b/edge-test/package.json index 9af286c0..3706d6ad 100644 --- a/edge-test/package.json +++ b/edge-test/package.json @@ -11,7 +11,7 @@ "author": "", "license": "ISC", "dependencies": { - "@amrc-factoryplus/edge-driver": "^0.0.1-bmz4", + "@amrc-factoryplus/edge-driver": "^0.0.2", "async": "^3.2.5", "mqtt": "^5.7.2" } From ffc0da875f7512df97e92931caf41deb4588c828 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Wed, 17 Jul 2024 12:42:43 +0100 Subject: [PATCH 26/36] Update TODO We are going to keep Devices limited to a single Connection. --- acs-edge/docs/TODO.md | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/acs-edge/docs/TODO.md b/acs-edge/docs/TODO.md index 759b762a..fa1eb83d 100644 --- a/acs-edge/docs/TODO.md +++ b/acs-edge/docs/TODO.md @@ -1,9 +1,5 @@ # TODO list for edge-split work -- [x] The Device subclasses need to go. Where they do work this needs to - move into the Connection. In particular some Devices handle - subscription tasks which should move to `startSubscription`. - - [ ] `DeviceConnection.readMetrics` accepts payload format / delimiter arguments. I don't think any of the drivers use them? This belongs EA-side. @@ -12,28 +8,5 @@ that it isn't used for this code path. Ideally we want all device writes to accept a plain Buffer as might be provided from a read. -- [x] The Connection currently handles the poll loop, as part of - `startSubscription`. - * For simple connections this is implemented in the base class and - should be handled by the Device (EA-side). - * Some Connections (MTConnect, OPCUA) can request polling in the - southbound protocol. The driver protocol needs extending to handle - this case. - - [ ] More generally, the Connections shouldn't see the Metrics at all. They should operate entirely on addresses. - -- [x] Multiple Devices may subscribe to a single Connection. The EA-side - Connection will need to track the current list of addresses we are - interested in and push it down to the driver. - -- [ ] Devices are currently linked to a single Connection. This is not - necessary, but means we need to: - * Add a `connection` property to each metric definition. - * Change the Device to poll via a central connection manifold rather - than via an individual connection. - * Supply data topic names to a Device in return for addresses when - it subscribes to the connection manifold. - * Poll the manifold using connection/datatopic pairs. - * Possibly the Device should always assume we are using a 'smart' - driver, and polling should be handled by the manifold? From dd88c7f927257b70dec6bb5be3518fd6a8e973dd Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Wed, 17 Jul 2024 13:46:42 +0100 Subject: [PATCH 27/36] Create a new LocalSecret CRD This requests krbkeys to create and update a random value in a local Secret. Edge Agents will use this to communicate with on-cluster drivers. --- acs-krb-keys-operator/crd/local-secret.yaml | 51 +++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 acs-krb-keys-operator/crd/local-secret.yaml diff --git a/acs-krb-keys-operator/crd/local-secret.yaml b/acs-krb-keys-operator/crd/local-secret.yaml new file mode 100644 index 00000000..d2a142b4 --- /dev/null +++ b/acs-krb-keys-operator/crd/local-secret.yaml @@ -0,0 +1,51 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: localsecrets.factoryplus.app.amrc.co.uk +spec: + group: factoryplus.app.amrc.co.uk + names: + kind: LocalSecret + plural: localsecrets + categories: + - all + scope: Namespaced + versions: + - name: v1 + served: true + storage: true + additionalPrinterColumns: + - name: Secret + jsonPath: ".spec.secret" + type: string + - name: Key + jsonPath: ".spec.key" + type: string + - name: Format + jsonPath: ".spec.format" + type: string + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + type: object + required: [secret, key, format] + properties: + secret: + description: The name of the Secret to edit. + type: string + key: + description: The key to create within the Secret. + type: string + format: + description: > + The format of the secret value. Currently must be Password. + type: string + enum: [Password] + status: + type: object + x-kubernetes-preserve-unknown-fields: true + subresources: + status: {} From 4814a2f81dcb4104de250ccc4f976d194e439f42 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Wed, 17 Jul 2024 14:53:16 +0100 Subject: [PATCH 28/36] Create local secrets --- .../lib/amrc/factoryplus/krbkeys/__init__.py | 26 ++++++++--------- .../lib/amrc/factoryplus/krbkeys/event.py | 18 +++++++++++- .../lib/amrc/factoryplus/krbkeys/spec.py | 28 +++++++++++++++++-- .../lib/amrc/factoryplus/krbkeys/util.py | 6 ++-- 4 files changed, 59 insertions(+), 19 deletions(-) diff --git a/acs-krb-keys-operator/lib/amrc/factoryplus/krbkeys/__init__.py b/acs-krb-keys-operator/lib/amrc/factoryplus/krbkeys/__init__.py index d79a9d2e..a458c622 100644 --- a/acs-krb-keys-operator/lib/amrc/factoryplus/krbkeys/__init__.py +++ b/acs-krb-keys-operator/lib/amrc/factoryplus/krbkeys/__init__.py @@ -16,15 +16,7 @@ from . import event from .context import Context -from .util import Identifiers, log - -CRD = (Identifiers.DOMAIN, Identifiers.CRD_VERSION, Identifiers.CRD_PLURAL) - -def kopf_crud (id, handler): - kopf.on.resume(*CRD, id=id)(handler) - kopf.on.create(*CRD, id=id)(handler) - kopf.on.update(*CRD, id=id)(handler) - kopf.on.delete(*CRD, id=id)(handler) +from .util import CRD, Identifiers, log class KrbKeys: def __init__ (self, env, **kw): @@ -42,16 +34,22 @@ def __init__ (self, env, **kw): uuids.App.SparkplugAddress, cluster)) \ .map(lambda addr: addr["group_id"]) + def kopf_crud (self, crd, id, ev): + kopf.on.resume(*crd, id=id)(self.process_event(ev)) + kopf.on.create(*crd, id=id)(self.process_event(ev)) + kopf.on.update(*crd, id=id)(self.process_event(ev)) + kopf.on.delete(*crd, id=id)(self.process_event(ev)) + def register_handlers (self): log("Registering handlers") - kopf_crud("rekey", self.process_event(event.Rekey)) - kopf.on.timer(*CRD, - id="trim_keys", + self.kopf_crud(CRD.krbkey, "rekey", event.Rekey) + kopf.on.timer(*CRD.krbkey, id="trim_keys", interval=self.expire_old_keys/2, labels={Identifiers.HAS_OLD_KEYS: "true"} )(self.process_event(event.TrimKeys)) - kopf_crud("account_uuid", self.process_event(event.AccUuid)) - kopf_crud("reconcile_account", self.process_event(event.Account)) + self.kopf_crud(CRD.krbkey, "account_uuid", event.AccUuid) + self.kopf_crud(CRD.krbkey, "reconcile_account", event.Account) + self.kopf_crud(CRD.local, "local_secret", event.LocalSecret) def run (self): self.register_handlers() diff --git a/acs-krb-keys-operator/lib/amrc/factoryplus/krbkeys/event.py b/acs-krb-keys-operator/lib/amrc/factoryplus/krbkeys/event.py index 96a723e1..cf874d93 100644 --- a/acs-krb-keys-operator/lib/amrc/factoryplus/krbkeys/event.py +++ b/acs-krb-keys-operator/lib/amrc/factoryplus/krbkeys/event.py @@ -8,7 +8,7 @@ from .account import FPAccount from .context import kk_ctx -from .spec import InternalSpec +from .spec import InternalSpec, LocalSpec from .util import Identifiers, dslice, log class KrbKeyEvent: @@ -155,3 +155,19 @@ def process (self): if self.new is not None: self.new.reconcile() + +class LocalSecret (KrbKeyEvent): + def __init__ (self, args): + super().__init__(args) + + self.old = LocalSpec.of(self.ns, args.get("old")) + self.new = Optional.of(self.reason) \ + .map(lambda r: None if r == "delete" else args.get("new")) \ + .flat_map(lambda a: LocalSpec.of(self.ns, a)) + + def process (self): + log(f"Process LocalSecret {self.old} -> {self.new}") + + self.old.if_present(lambda s: s.remove()) + self.new.if_present(lambda s: s.update()) + diff --git a/acs-krb-keys-operator/lib/amrc/factoryplus/krbkeys/spec.py b/acs-krb-keys-operator/lib/amrc/factoryplus/krbkeys/spec.py index 63f27234..e55a2819 100644 --- a/acs-krb-keys-operator/lib/amrc/factoryplus/krbkeys/spec.py +++ b/acs-krb-keys-operator/lib/amrc/factoryplus/krbkeys/spec.py @@ -4,13 +4,15 @@ from enum import Enum import logging +from optional import Optional +import secrets import typing from uuid import UUID from . import keyops from .context import kk_ctx -from .secrets import SecretRef -from .util import Identifiers, dslice, fields, hidden, log +from .secrets import SecretRef, LocalSecret +from .util import Identifiers, dslice, fields, hidden, immutable, log @fields class InternalSpec: @@ -110,3 +112,25 @@ def trim_keys (self): self.secret.write(status.secret) return status + +@immutable +class LocalSpec: + secret: LocalSecret + format: str + + @classmethod + def of (cls, ns, arg): + return Optional.of(arg) \ + .map(lambda ob: ob.get("spec")) \ + .map(lambda spec: cls( + secret=LocalSecret(ns=ns, + name=spec["secret"], + key=spec["key"]), + format=spec.get("format", "Password"))) + + def update (self): + passwd = secrets.token_urlsafe().encode() + self.secret.write(passwd) + + def remove (self): + self.secret.remove() diff --git a/acs-krb-keys-operator/lib/amrc/factoryplus/krbkeys/util.py b/acs-krb-keys-operator/lib/amrc/factoryplus/krbkeys/util.py index bb6e0d45..cc089f5a 100644 --- a/acs-krb-keys-operator/lib/amrc/factoryplus/krbkeys/util.py +++ b/acs-krb-keys-operator/lib/amrc/factoryplus/krbkeys/util.py @@ -17,8 +17,6 @@ class Identifiers: DOMAIN = "factoryplus.app.amrc.co.uk" APP = "krbkeys" APPID = f"{APP}.{DOMAIN}" - CRD_PLURAL = "kerberos-keys" - CRD_VERSION = "v1" FORCE_REKEY = f"{APPID}/force-rekey" HAS_OLD_KEYS = f"{APPID}/has-old-keys" @@ -26,6 +24,10 @@ class Identifiers: MANAGED_BY = "app.kubernetes.io/managed-by" +class CRD: + krbkey = (Identifiers.DOMAIN, "v1", "kerberos-keys") + local = (Identifiers.DOMAIN, "v1", "localsecrets") + def dslice (dct, *args): if dct is None: return None From 61e616033b4531d6bfb84b6a8db5c1077f4492b9 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Fri, 12 Jul 2024 15:10:01 +0100 Subject: [PATCH 29/36] Deploy the EA side of the drivers * Give the EA access to a Secret for the driver passwords. This will need to be populated by krbkeys, I think. * Create a Service for the EA broker. --- .../charts/edge-agent/templates/edge-agent.yaml | 10 ++++++++++ .../charts/edge-agent/templates/service.yaml | 15 +++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 edge-helm-charts/charts/edge-agent/templates/service.yaml diff --git a/edge-helm-charts/charts/edge-agent/templates/edge-agent.yaml b/edge-helm-charts/charts/edge-agent/templates/edge-agent.yaml index b3ace589..c88de0ae 100644 --- a/edge-helm-charts/charts/edge-agent/templates/edge-agent.yaml +++ b/edge-helm-charts/charts/edge-agent/templates/edge-agent.yaml @@ -49,6 +49,10 @@ spec: secretKeyRef: name: edge-agent-secrets-{{ .Values.uuid }} key: keytab + - name: EDGE_MQTT + value: "mqtt://0.0.0.0" + - name: EDGE_PASSWORDS + value: "/usr/app/driver-passwords" resources: limits: memory: {{ .Values.limits.memory | quote }} @@ -61,6 +65,8 @@ spec: readOnly: true - mountPath: /home/node/.config name: local-config + - mountPath: /usr/app/driver-passwords + name: driver-passwords volumes: - name: edge-agent-sensitive-information secret: @@ -68,6 +74,10 @@ spec: secretName: edge-agent-sensitive-information-{{ .Values.uuid }} - name: local-config emptyDir: + - name: driver-passwords + secret: + optional: true + secretName: driver-passwords-{{ .Values.uuid }} --- apiVersion: factoryplus.app.amrc.co.uk/v1 kind: SparkplugNode diff --git a/edge-helm-charts/charts/edge-agent/templates/service.yaml b/edge-helm-charts/charts/edge-agent/templates/service.yaml new file mode 100644 index 00000000..a0d0da6d --- /dev/null +++ b/edge-helm-charts/charts/edge-agent/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + namespace: {{ .Release.Namespace }} + name: edge-agent-{{ .Values.uuid }} +spec: + selector: + factory-plus.service: edge-mqtt-{{ .Values.uuid }} + internalTrafficPolicy: Local + ports: + - name: mqtt + port: 1883 +{{- if .Values.externalIPs }} + externalIPs: {{ .Values.externalIPs }} +{{- end }} From bd0339d6b7b880e8cc52e44a6f6dd3ff818969be Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Fri, 12 Jul 2024 15:50:41 +0100 Subject: [PATCH 30/36] Edge Agent pods don't have factory-plus.service --- edge-helm-charts/charts/edge-agent/templates/service.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/edge-helm-charts/charts/edge-agent/templates/service.yaml b/edge-helm-charts/charts/edge-agent/templates/service.yaml index a0d0da6d..9a55413d 100644 --- a/edge-helm-charts/charts/edge-agent/templates/service.yaml +++ b/edge-helm-charts/charts/edge-agent/templates/service.yaml @@ -5,7 +5,8 @@ metadata: name: edge-agent-{{ .Values.uuid }} spec: selector: - factory-plus.service: edge-mqtt-{{ .Values.uuid }} + factory-plus.app: edge-agent + factory-plus.nodeUuid: {{ .Values.uuid }} internalTrafficPolicy: Local ports: - name: mqtt From 6fa78b6af61d304d814e1f9ad4d5a45b3436ac03 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Mon, 15 Jul 2024 10:42:46 +0100 Subject: [PATCH 31/36] Deploy drivers in the edge agent pod Given that all drivers are deployed identically, and that in general we want to run them on the same host as the edge agent, deploy them as part of the edge agent pod and from the same Helm chart. This does not preclude external drivers, but would need additional configuration to set up external IPs and to listen to the wildcard address. --- .../charts/edge-agent/templates/edge-agent.yaml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/edge-helm-charts/charts/edge-agent/templates/edge-agent.yaml b/edge-helm-charts/charts/edge-agent/templates/edge-agent.yaml index c88de0ae..3e8f12fa 100644 --- a/edge-helm-charts/charts/edge-agent/templates/edge-agent.yaml +++ b/edge-helm-charts/charts/edge-agent/templates/edge-agent.yaml @@ -50,7 +50,7 @@ spec: name: edge-agent-secrets-{{ .Values.uuid }} key: keytab - name: EDGE_MQTT - value: "mqtt://0.0.0.0" + value: "mqtt://localhost" - name: EDGE_PASSWORDS value: "/usr/app/driver-passwords" resources: @@ -67,6 +67,20 @@ spec: name: local-config - mountPath: /usr/app/driver-passwords name: driver-passwords +{{- range $name, $image := .Values.drivers }} + - name: "driver-{{ $name | lower }}" + image: "{{ $image }}" + env: + - name: EDGE_MQTT + value: "mqtt://localhost" + - name: EDGE_USERNAME + value: "{{ $name }}" + - name: EDGE_PASSWORD + valueFrom: + secretKeyRef: + name: "driver-passwords-{{ $.Values.uuid }}" + key: "{{ $name }}" +{{- end }} volumes: - name: edge-agent-sensitive-information secret: From 6723cb6f6f2861218673db00fedbc37680cdac5c Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Tue, 16 Jul 2024 16:10:44 +0100 Subject: [PATCH 32/36] Make edge driver images more configurable Include driver images in the `image` values item. Pull them in by name instead of with a full image name. Support defaults for registry and repository. Pass a VERBOSE environment variable to the drivers. --- .../charts/edge-agent/templates/_helpers.tpl | 11 ++++++----- .../charts/edge-agent/templates/edge-agent.yaml | 6 ++++-- edge-helm-charts/charts/edge-agent/values.yaml | 16 +++++++++++++--- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/edge-helm-charts/charts/edge-agent/templates/_helpers.tpl b/edge-helm-charts/charts/edge-agent/templates/_helpers.tpl index 1f533b39..da973f8e 100644 --- a/edge-helm-charts/charts/edge-agent/templates/_helpers.tpl +++ b/edge-helm-charts/charts/edge-agent/templates/_helpers.tpl @@ -1,8 +1,9 @@ -{{/* -Define the image for a container. -*/}} {{- define "edge-agent.image" -}} -image: "{{ .registry }}/{{ .repository }}:{{ .tag }}" -imagePullPolicy: {{ .pullPolicy }} +{{- $root := index . 0 -}} +{{- $key := index . 1 -}} +{{- $image := $root.Values.image -}} +{{- $spec := merge (get $image $key) $image.default -}} +image: "{{ $spec.registry }}/{{ $spec.repository }}:{{ $spec.tag }}" +imagePullPolicy: {{ $spec.pullPolicy }} {{- end }} diff --git a/edge-helm-charts/charts/edge-agent/templates/edge-agent.yaml b/edge-helm-charts/charts/edge-agent/templates/edge-agent.yaml index 3e8f12fa..b48acdcd 100644 --- a/edge-helm-charts/charts/edge-agent/templates/edge-agent.yaml +++ b/edge-helm-charts/charts/edge-agent/templates/edge-agent.yaml @@ -32,7 +32,7 @@ spec: {{ end }} containers: - name: edge-agent -{{ include "edge-agent.image" .Values.image.edgeAgent | indent 10 }} +{{ list . "edgeAgent" | include "edge-agent.image" | indent 10 }} env: - name: DEBUG value: {{ .Values.debug | quote }} @@ -69,7 +69,7 @@ spec: name: driver-passwords {{- range $name, $image := .Values.drivers }} - name: "driver-{{ $name | lower }}" - image: "{{ $image }}" +{{ list $ $image | include "edge-agent.image" | indent 10 }} env: - name: EDGE_MQTT value: "mqtt://localhost" @@ -80,6 +80,8 @@ spec: secretKeyRef: name: "driver-passwords-{{ $.Values.uuid }}" key: "{{ $name }}" + - name: VERBOSE + value: "{{ $.Values.verbosity }}" {{- end }} volumes: - name: edge-agent-sensitive-information diff --git a/edge-helm-charts/charts/edge-agent/values.yaml b/edge-helm-charts/charts/edge-agent/values.yaml index 80d265df..6e842e51 100644 --- a/edge-helm-charts/charts/edge-agent/values.yaml +++ b/edge-helm-charts/charts/edge-agent/values.yaml @@ -1,10 +1,20 @@ image: - # Parameters for the Edge Agent image to pull - edgeAgent: + # Default image parameters. These can be overidden per-image. + default: registry: ghcr.io/amrc-factoryplus - repository: acs-edge tag: v3.1.2 pullPolicy: IfNotPresent + # Edge Agent image to pull + edgeAgent: + repository: acs-edge + modbus: + repository: edge-modbus + test: + repository: edge-test + # Further image names for drivers as needed +drivers: + # An object mapping connection names to images from the list above. + #Test: test debug: false verbosity: ALL,!token,!service,!sparkplug poll_int: 10 From 7da05ec391b44eac6e924d5663b0206fc574115b Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Wed, 17 Jul 2024 13:49:49 +0100 Subject: [PATCH 33/36] Pull in LocalSecret CRD --- .../edge-cluster/crds/local-secret.yaml | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 edge-helm-charts/charts/edge-cluster/crds/local-secret.yaml diff --git a/edge-helm-charts/charts/edge-cluster/crds/local-secret.yaml b/edge-helm-charts/charts/edge-cluster/crds/local-secret.yaml new file mode 100644 index 00000000..d2a142b4 --- /dev/null +++ b/edge-helm-charts/charts/edge-cluster/crds/local-secret.yaml @@ -0,0 +1,51 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: localsecrets.factoryplus.app.amrc.co.uk +spec: + group: factoryplus.app.amrc.co.uk + names: + kind: LocalSecret + plural: localsecrets + categories: + - all + scope: Namespaced + versions: + - name: v1 + served: true + storage: true + additionalPrinterColumns: + - name: Secret + jsonPath: ".spec.secret" + type: string + - name: Key + jsonPath: ".spec.key" + type: string + - name: Format + jsonPath: ".spec.format" + type: string + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + type: object + required: [secret, key, format] + properties: + secret: + description: The name of the Secret to edit. + type: string + key: + description: The key to create within the Secret. + type: string + format: + description: > + The format of the secret value. Currently must be Password. + type: string + enum: [Password] + status: + type: object + x-kubernetes-preserve-unknown-fields: true + subresources: + status: {} From 0ff9d588063d4a0c326ddd737a3491473f0dab03 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Wed, 17 Jul 2024 15:42:05 +0100 Subject: [PATCH 34/36] Grant krbkeys access to LocalSecrets --- edge-helm-charts/charts/edge-cluster/templates/krb-keys.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/edge-helm-charts/charts/edge-cluster/templates/krb-keys.yaml b/edge-helm-charts/charts/edge-cluster/templates/krb-keys.yaml index 003c2d03..23ad0acf 100644 --- a/edge-helm-charts/charts/edge-cluster/templates/krb-keys.yaml +++ b/edge-helm-charts/charts/edge-cluster/templates/krb-keys.yaml @@ -108,10 +108,10 @@ metadata: name: krb-keys rules: - apiGroups: [factoryplus.app.amrc.co.uk] - resources: [kerberos-keys] + resources: [kerberos-keys, localsecrets] verbs: [list, get, watch, patch] - apiGroups: [factoryplus.app.amrc.co.uk] - resources: [kerberos-keys/status] + resources: [kerberos-keys/status, localsecrets/status] verbs: [list, get, create, update, delete, watch, patch] - apiGroups: [""] resources: [secrets] From 8761822d4368254b7b8705a2498cb51d041a0d44 Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Wed, 17 Jul 2024 15:56:09 +0100 Subject: [PATCH 35/36] Request passwords for drivers --- .../charts/edge-agent/templates/edge-agent.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/edge-helm-charts/charts/edge-agent/templates/edge-agent.yaml b/edge-helm-charts/charts/edge-agent/templates/edge-agent.yaml index b48acdcd..65ab1749 100644 --- a/edge-helm-charts/charts/edge-agent/templates/edge-agent.yaml +++ b/edge-helm-charts/charts/edge-agent/templates/edge-agent.yaml @@ -105,3 +105,15 @@ spec: edgeAgent: true secrets: - edge-agent-sensitive-information-{{ .Values.uuid }} +{{ range $name, $image := .Values.drivers }} +--- +apiVersion: factoryplus.app.amrc.co.uk/v1 +kind: LocalSecret +metadata: + namespace: {{ $.Release.Namespace }} + name: "driver-passwords-{{ $.Values.uuid }}-{{ $name | lower }}" +spec: + format: Password + secret: "driver-passwords-{{ $.Values.uuid }}" + key: "{{ $name }}" +{{- end }} From 8bc8e371fe35f4e66895ec05eb6814bf217f9c6b Mon Sep 17 00:00:00 2001 From: Ben Morrow Date: Thu, 1 Aug 2024 09:50:41 +0100 Subject: [PATCH 36/36] Support external drivers correctly For now we have `externalTrafficPolicy: Local` which will enforce that external drivers have to connect to the host the edge-agent is running on. K8s does allow us to be more flexible than that but I'm not sure if we want to. --- edge-helm-charts/charts/edge-agent/templates/edge-agent.yaml | 4 ++++ edge-helm-charts/charts/edge-agent/templates/service.yaml | 3 ++- edge-helm-charts/charts/edge-agent/values.yaml | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/edge-helm-charts/charts/edge-agent/templates/edge-agent.yaml b/edge-helm-charts/charts/edge-agent/templates/edge-agent.yaml index 65ab1749..8ab4a1f2 100644 --- a/edge-helm-charts/charts/edge-agent/templates/edge-agent.yaml +++ b/edge-helm-charts/charts/edge-agent/templates/edge-agent.yaml @@ -50,7 +50,11 @@ spec: name: edge-agent-secrets-{{ .Values.uuid }} key: keytab - name: EDGE_MQTT +{{- if .Values.externalIPs }} + value: "mqtt://0.0.0.0" +{{- else }} value: "mqtt://localhost" +{{- end }} - name: EDGE_PASSWORDS value: "/usr/app/driver-passwords" resources: diff --git a/edge-helm-charts/charts/edge-agent/templates/service.yaml b/edge-helm-charts/charts/edge-agent/templates/service.yaml index 9a55413d..aaaa93f3 100644 --- a/edge-helm-charts/charts/edge-agent/templates/service.yaml +++ b/edge-helm-charts/charts/edge-agent/templates/service.yaml @@ -1,3 +1,4 @@ +{{- if .Values.externalIPs }} apiVersion: v1 kind: Service metadata: @@ -8,9 +9,9 @@ spec: factory-plus.app: edge-agent factory-plus.nodeUuid: {{ .Values.uuid }} internalTrafficPolicy: Local + externalTrafficPolicy: Local ports: - name: mqtt port: 1883 -{{- if .Values.externalIPs }} externalIPs: {{ .Values.externalIPs }} {{- end }} diff --git a/edge-helm-charts/charts/edge-agent/values.yaml b/edge-helm-charts/charts/edge-agent/values.yaml index 6e842e51..abf84988 100644 --- a/edge-helm-charts/charts/edge-agent/values.yaml +++ b/edge-helm-charts/charts/edge-agent/values.yaml @@ -15,6 +15,8 @@ image: drivers: # An object mapping connection names to images from the list above. #Test: test +# Make the driver interface available externally. +#externalIPs: [] debug: false verbosity: ALL,!token,!service,!sparkplug poll_int: 10