diff --git a/acs-edge/Makefile b/acs-edge/Makefile index 9d3cc657..979616cf 100644 --- a/acs-edge/Makefile +++ b/acs-edge/Makefile @@ -5,3 +5,10 @@ repo?=acs-edge # Don't set k8s.deployment, the deployment doesn't have a fixed name. include ${mk}/acs.js.mk + +local.build: + npm install + 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/docs/TODO.md b/acs-edge/docs/TODO.md new file mode 100644 index 00000000..fa1eb83d --- /dev/null +++ b/acs-edge/docs/TODO.md @@ -0,0 +1,12 @@ +# TODO list for edge-split work + +- [ ] `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. + +- [ ] More generally, the Connections shouldn't see the Metrics at all. + They should operate entirely on addresses. diff --git a/acs-edge/docs/edge-split.md b/acs-edge/docs/edge-split.md new file mode 100644 index 00000000..288585e2 --- /dev/null +++ b/acs-edge/docs/edge-split.md @@ -0,0 +1,293 @@ +# 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 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. 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) + +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. Where +necessary the data topic name `Data` will also 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/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 +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. + +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 +required by the current address configuration. The driver is not +expected to avoid sending duplicate data packets. + +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 + +The driver publishes to this topic to report an error with a particular +address. + +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 +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 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 +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. diff --git a/acs-edge/lib/device.ts b/acs-edge/lib/device.ts index 1032544e..4b4e60da 100644 --- a/acs-edge/lib/device.ts +++ b/acs-edge/lib/device.ts @@ -3,7 +3,9 @@ * Copyright 2023 AMRC */ -import {log} from "./helpers/log.js"; +import { + log, logf +} from "./helpers/log.js"; import * as fs from "fs"; import {SparkplugNode} from "./sparkplugNode.js"; import { @@ -44,6 +46,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. @@ -56,7 +59,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'); } @@ -93,6 +99,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 @@ -102,8 +126,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); @@ -116,9 +141,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(); } @@ -135,7 +162,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 @@ -144,7 +171,7 @@ export abstract 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 @@ -229,8 +256,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; @@ -239,9 +266,9 @@ export abstract 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 @@ -326,7 +353,7 @@ export abstract 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(); } /** @@ -483,7 +510,7 @@ export abstract 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/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 0ba1ca62..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); } /** @@ -107,7 +119,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) { @@ -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/driver.ts b/acs-edge/lib/devices/driver.ts new file mode 100644 index 00000000..944ae821 --- /dev/null +++ b/acs-edge/lib/devices/driver.ts @@ -0,0 +1,210 @@ +/* + * 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) { + const ost = this.status; + this.status = status; + log(`DRIVER [${this.id}]: status ${ost} -> ${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": + if (ost != "UP") + this.emit("open"); + break; + case "DOWN": + case "CONF": + case "CONN": + case "AUTH": + case "ERR": + if (ost == "UP") + this.emit("close"); + break; + } + } + + #send_addrs () { + const addrs = { + version: 1, + 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/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 7e20d8d9..730d2125 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) => { @@ -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/driverBroker.ts b/acs-edge/lib/driverBroker.ts new file mode 100644 index 00000000..1276b763 --- /dev/null +++ b/acs-edge/lib/driverBroker.ts @@ -0,0 +1,155 @@ +/* + * 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+)/(\\w+)(?:/(\\w+))?$`); + +function log (f, ...a) { + const msg = util.format(f, ...a); + console.log("DRIVER: %s", msg); +} + +interface ACL { + publish: RegExp, + subscribe: RegExp, +} + +export class DriverBroker extends EventEmitter { + broker: Aedes + passwords: string + acl: 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.acl = 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); + return new Promise(resolve => { + srv.once("listening", () => { + log("Listening: %o", srv.address()); + resolve(); + }); + 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.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); + } + + 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.acl.get(id)!.publish.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.acl.get(id)!.subscribe.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, 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..18dc10b8 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", diff --git a/acs-edge/lib/sparkplugNode.ts b/acs-edge/lib/sparkplugNode.ts index b3582fa4..295e9dd5 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, @@ -416,8 +416,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 6eaaac1d..ed703602 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,24 +17,23 @@ import {ServiceClient} from "@amrc-factoryplus/utilities"; import {validateConfig} from '../utils/CentralConfig.js'; import {reHashConf} from "../utils/FormatConfig.js"; -// Import device connections +import {Device, deviceOptions} from "./device.js"; +import {DriverBroker} from "./driverBroker.js"; import {SparkplugNode} from "./sparkplugNode.js"; +import * as UUIDs from "./uuids.js"; -// Import devices -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 {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"; + +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"; +import {DriverConnection} from "./devices/driver.js"; /** * Translator class basically turns config file into instantiated classes @@ -43,7 +46,6 @@ export interface translatorConf { } interface deviceInfo { - type: any, connection: any; connectionDetails: any } @@ -55,6 +57,7 @@ export class Translator extends EventEmitter { */ sparkplugNode!: SparkplugNode fplus: ServiceClient + broker: DriverBroker pollInt: number connections: { @@ -64,10 +67,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 = {}; @@ -89,12 +93,18 @@ 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)); // Setup Sparkplug node handlers this.setupSparkplug(); + } catch (e: any) { log(`Error starting translator: ${e.message}`); console.error((e as Error).stack); @@ -119,6 +129,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(); @@ -177,10 +189,18 @@ 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], + /* 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 deviceInfo.type(this.sparkplugNode, newConn, devConf); + this.devices[devConf.deviceId] = new Device( + this.sparkplugNode, newConn, devConf); }); // What to do when the connection is open @@ -192,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); }) @@ -215,37 +237,42 @@ 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' } + case "Driver": + return { + connection: DriverConnection, + connectionDetails: "DriverDetails", + }; default: return; } @@ -308,7 +335,7 @@ export class Translator extends EventEmitter { if (valid) { try { config = JSON.parse(secretReplacedConfig); - valid = validateConfig(config); + //valid = validateConfig(config); } catch { valid = false; } @@ -348,4 +375,4 @@ Trying again in ${interval} seconds...`); await timers.setTimeout(interval * 1000); } } -} \ No newline at end of file +} diff --git a/acs-edge/package-lock.json b/acs-edge/package-lock.json index 791c0707..c69ea9b5 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", @@ -1885,6 +1886,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", @@ -3643,6 +3778,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", @@ -3943,6 +4086,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", @@ -4633,6 +4801,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", @@ -6371,6 +6580,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.7.2", "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.7.2.tgz", @@ -8684,6 +8913,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", @@ -8935,6 +9172,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", @@ -8944,6 +9189,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.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -10113,6 +10367,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 78a7d495..1caad9c1 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", 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" ] } 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: {} 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 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 b3ace589..8ab4a1f2 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 }} @@ -49,6 +49,14 @@ spec: secretKeyRef: 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: limits: memory: {{ .Values.limits.memory | quote }} @@ -61,6 +69,24 @@ spec: readOnly: true - mountPath: /home/node/.config name: local-config + - mountPath: /usr/app/driver-passwords + name: driver-passwords +{{- range $name, $image := .Values.drivers }} + - name: "driver-{{ $name | lower }}" +{{ list $ $image | include "edge-agent.image" | indent 10 }} + env: + - name: EDGE_MQTT + value: "mqtt://localhost" + - name: EDGE_USERNAME + value: "{{ $name }}" + - name: EDGE_PASSWORD + valueFrom: + secretKeyRef: + name: "driver-passwords-{{ $.Values.uuid }}" + key: "{{ $name }}" + - name: VERBOSE + value: "{{ $.Values.verbosity }}" +{{- end }} volumes: - name: edge-agent-sensitive-information secret: @@ -68,6 +94,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 @@ -79,3 +109,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 }} 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..aaaa93f3 --- /dev/null +++ b/edge-helm-charts/charts/edge-agent/templates/service.yaml @@ -0,0 +1,17 @@ +{{- if .Values.externalIPs }} +apiVersion: v1 +kind: Service +metadata: + namespace: {{ .Release.Namespace }} + name: edge-agent-{{ .Values.uuid }} +spec: + selector: + factory-plus.app: edge-agent + factory-plus.nodeUuid: {{ .Values.uuid }} + internalTrafficPolicy: Local + externalTrafficPolicy: Local + ports: + - name: mqtt + port: 1883 + 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 a1f05494..98f8144e 100644 --- a/edge-helm-charts/charts/edge-agent/values.yaml +++ b/edge-helm-charts/charts/edge-agent/values.yaml @@ -1,10 +1,22 @@ 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.2.0 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 +# Make the driver interface available externally. +#externalIPs: [] debug: false verbosity: ALL,!token,!service,!sparkplug poll_int: 10 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: {} 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] diff --git a/edge-modbus/.gitignore b/edge-modbus/.gitignore new file mode 100644 index 00000000..6cb772be --- /dev/null +++ b/edge-modbus/.gitignore @@ -0,0 +1,2 @@ +node_modules +tmp 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 diff --git a/edge-modbus/bin/driver.js b/edge-modbus/bin/driver.js new file mode 100644 index 00000000..e41eb62c --- /dev/null +++ b/edge-modbus/bin/driver.js @@ -0,0 +1,14 @@ +/* AMRC Connectivity Stack + * Modbus Edge Agent driver + * Copyright 2024 AMRC + */ + +import { PolledDriver } from "@amrc-factoryplus/edge-driver"; +import { modbusHandler } from "../lib/modbus.js"; + +const drv = new PolledDriver({ + env: process.env, + handler: modbusHandler, + serial: true, +}); +drv.run(); diff --git a/edge-modbus/lib/modbus.js b/edge-modbus/lib/modbus.js new file mode 100644 index 00000000..3ea4cd40 --- /dev/null +++ b/edge-modbus/lib/modbus.js @@ -0,0 +1,103 @@ +/* AMRC Connectivity Stack + * Modbus Edge Agent driver + * Copyright 2024 AMRC + */ + +import ModbusRTU from "modbus-serial"; + +const RECONNECT = 5000; + +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.log = driver.debug.bound("modbus"); + + this.client = new ModbusRTU(); + this.on_close = () => this.reconnect(); + } + + run () { + const { driver, client } = this; + const { host, port } = this.conf; + + client.on("close", this.on_close); + + client.connectTCP(host, { port }) + .then(() => driver.setStatus("UP")) + .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 () { + const { client, driver } = this; + + this.log("Modbus connection closed"); + setTimeout(() => { + this.log("Reconnecting to modbus"); + client.open(e => { + driver.setStatus(e ? "CONN" : "UP"); + if (e) { + this.log("Failed to connect to modbus: %s", e); + this.reconnect(); + } + }); + }, RECONNECT); + } + + 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) 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).run(); +} diff --git a/edge-modbus/package-lock.json b/edge-modbus/package-lock.json new file mode 100644 index 00000000..287249c4 --- /dev/null +++ b/edge-modbus/package-lock.json @@ -0,0 +1,1271 @@ +{ + "name": "edge-modbus", + "version": "0.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "edge-modbus", + "version": "0.0.0", + "license": "ISC", + "dependencies": { + "@amrc-factoryplus/edge-driver": "^0.0.2", + "modbus-serial": "^8.0.17" + } + }, + "node_modules/@amrc-factoryplus/edge-driver": { + "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.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.8.tgz", + "integrity": "sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==", + "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.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.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.11", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.11.tgz", + "integrity": "sha512-4+q7P5h3SpJxaBft0Dzpbr6lmMaqh0Jr2tbhJZ/luAwvD7ohSCniYkwz/pLxuT2h0EOa6QADgJj1Ko+TzRfZ+w==", + "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": { + "@amrc-factoryplus/edge-driver": { + "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.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.8.tgz", + "integrity": "sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==", + "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.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.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.11", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.11.tgz", + "integrity": "sha512-4+q7P5h3SpJxaBft0Dzpbr6lmMaqh0Jr2tbhJZ/luAwvD7ohSCniYkwz/pLxuT2h0EOa6QADgJj1Ko+TzRfZ+w==", + "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..1dccdd9a --- /dev/null +++ b/edge-modbus/package.json @@ -0,0 +1,20 @@ +{ + "name": "edge-modbus", + "version": "0.0.0", + "description": "", + "main": "index.js", + "type": "module", + "directories": { + "doc": "docs" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "modbus-serial": "^8.0.17", + "@amrc-factoryplus/edge-driver": "^0.0.2" + } +} 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/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 new file mode 100644 index 00000000..dc93af92 --- /dev/null +++ b/edge-test/bin/driver.js @@ -0,0 +1,14 @@ +/* AMRC Connectivity Stack + * Testing Edge Agent driver + * Copyright 2024 AMRC + */ + +import { PolledDriver } from "@amrc-factoryplus/edge-driver"; +import { handleTest } from "../lib/test.js"; + +const drv = new PolledDriver({ + env: process.env, + handler: handleTest, + serial: false, +}); +drv.run(); diff --git a/edge-test/lib/test.js b/edge-test/lib/test.js new file mode 100644 index 00000000..7081a332 --- /dev/null +++ b/edge-test/lib/test.js @@ -0,0 +1,52 @@ +/* + * 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 { + 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) { + const h = new TestHandler(); + driver.setStatus("UP"); + return h; +} diff --git a/edge-test/package-lock.json b/edge-test/package-lock.json new file mode 100644 index 00000000..d67012a5 --- /dev/null +++ b/edge-test/package-lock.json @@ -0,0 +1,813 @@ +{ + "name": "edge-modbus", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "edge-modbus", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@amrc-factoryplus/edge-driver": "^0.0.2", + "async": "^3.2.5", + "mqtt": "^5.7.2" + } + }, + "node_modules/@amrc-factoryplus/edge-driver": { + "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.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.8.tgz", + "integrity": "sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@types/node": { + "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.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.11", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.11.tgz", + "integrity": "sha512-4+q7P5h3SpJxaBft0Dzpbr6lmMaqh0Jr2tbhJZ/luAwvD7ohSCniYkwz/pLxuT2h0EOa6QADgJj1Ko+TzRfZ+w==", + "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/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/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.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": { + "@amrc-factoryplus/edge-driver": { + "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.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.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.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.11", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.11.tgz", + "integrity": "sha512-4+q7P5h3SpJxaBft0Dzpbr6lmMaqh0Jr2tbhJZ/luAwvD7ohSCniYkwz/pLxuT2h0EOa6QADgJj1Ko+TzRfZ+w==", + "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==" + }, + "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==" + }, + "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.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 new file mode 100644 index 00000000..3706d6ad --- /dev/null +++ b/edge-test/package.json @@ -0,0 +1,18 @@ +{ + "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": { + "@amrc-factoryplus/edge-driver": "^0.0.2", + "async": "^3.2.5", + "mqtt": "^5.7.2" + } +} 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