From 53056a3362046062b34f6e91b8668f4413f1d5a4 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Wed, 1 Nov 2023 14:18:00 +0300 Subject: [PATCH] progress --- controllers/modbus.controller.js | 59 +++++++++++------------------ data.db | Bin 20480 -> 20480 bytes events/index.js | 3 -- index.d.ts | 20 ++++++++++ migrations/001-init.sql | 11 +++--- public/index.html | 23 ++++++----- public/js/app.js | 44 ++++++++++++--------- services/index.js | 1 + services/modbus.service.js | 39 +++++++++++++++++++ utils/index.js | 1 + utils/number.js | 19 ++++++++++ validators/modbus-device.schema.js | 2 +- 12 files changed, 148 insertions(+), 74 deletions(-) create mode 100644 services/modbus.service.js create mode 100644 utils/index.js create mode 100644 utils/number.js diff --git a/controllers/modbus.controller.js b/controllers/modbus.controller.js index 708ccde..c083bcb 100644 --- a/controllers/modbus.controller.js +++ b/controllers/modbus.controller.js @@ -1,6 +1,7 @@ import db from "#root/config/database.config.js"; import modbusClient from "#root/config/modbus-client.config.js"; -import { appService } from "#root/services/index.js"; +import * as utils from '#root/utils/index.js' +import { appService, modbusService } from "#root/services/index.js"; export const modbusController = { /** @@ -204,49 +205,35 @@ export const modbusController = { async streamData(req, res, next) { try { if (!modbusClient.isOpen) return res.status(503).json({ error: { message: "Modbus connection closed." } }); + + const { slave_id } = req.query; + + if (!slave_id) + return res.status(400).json({ error: { message: "'slave_id' parameter is required." } }); + + const device = await db.get(`SELECT * FROM "modbus_slaves" WHERE id = ?`, [slave_id]); + + if (!device) + return res.status(404).json({ error: { message: "Device not found." } }); - if (!req.query.slave_id) return res.status(400).json({ error: { message: "'slave_id' parameter is required." } }); - - const device = await db.get(`SELECT * FROM "modbus_slaves" WHERE id = ?`, [req.query.slave_id]); - const displayValues = await db.all(`SELECT * FROM "display_values" WHERE slave_id = ?`, [req.query.slave_id]) - - if (!device) return res.status(404).json({ error: { message: "Device not found." } }); + device.display_values = await db.all(`SELECT * FROM "display_values" WHERE slave_id = ?`, [slave_id]) res.setHeader("Cache-Control", "no-cache"); res.setHeader("Content-Type", "text/event-stream"); res.setHeader("Connection", "keep-alive"); - let interval = setInterval(async () => { - let graphValue = 0; - let len = 1; // 16 bits default - - for (const value of displayValues) { - value.data = Buffer.from((await modbusClient.readHoldingRegisters(value.reg_addr, 1)).buffer).readUInt16BE(); - } - - if (device.g_display_reg_format === 32) len = 2; - // ...64, 128, 256... - - const result = await modbusClient.readHoldingRegisters(device.g_display_reg_addr, len); - - switch (device.g_display_reg_format) { - case 32: - graphValue = Buffer.from(result.buffer).readUInt32BE(); - break; - case 16: - graphValue = Buffer.from(result.buffer).readUint16BE(); - break; - default: - break; + const streamInterval = setInterval(async () => { + try { + const data = await modbusService.readDataFromDevice(device, 'HR'); + res.write("event: message\n"); + res.write(`data: ${JSON.stringify(data)}\n\n`); + } catch (error) { + clearInterval(streamInterval); + next(error); } - - res.write("event: message\n"); - res.write(`data: ${JSON.stringify({ - graph: { value: graphValue, format: device.g_display_reg_format }, - displayValues - })}\n\n`); }, 1000); - req.on("close", () => clearInterval(interval)); + + req.on("close", () => clearInterval(streamInterval)); } catch (error) { next(error); } diff --git a/data.db b/data.db index 78ab51f34c4a8a080d5aabb01cbe6e5c70c70884..4549151f8cae9dfc55f623869dd463349e395a59 100644 GIT binary patch delta 534 zcmZozz}T>Wae|Z(vj_tN11k{2z@>>gMq)rwy|kCSKrt4MEe!mTJZW6M99uRvI&e(B z$)U~2vYC}LmyxSQjG0|rU7fK#dvgt!JtJ3`W013lV~~ca=HzlIc~hlOPeU^$9R(#Q z73yhh1m;30w*Vlmsj1-N>gE{g6QTf7K6#~7(d0B9Wk%-BT|70+EL9AwRT~=%Sox+h zGqN*?F1K#-&&Wae|Z((<253238=3fin|zjKqMVdTB3tfnqEiHyQXNdD6IwIBsqhR7mBR zyoE!Xk$Lk2j$B5r=0aw6admaZcE!!%T=tBU1-WG=$4PNbmXHyjyih7{avF~^Bg^J4 zo*HJB)eNkwH#RP0<(m@3$j%_TEV9WzGcU7bvY>px<{P5vEXaB{FO*ur#AIkT*Fasm6cwTC` zz~nA}Ie$rJb_RJyMrjV0)UwRvR0U;YW&xkXq|_V*V+kgp6epuJCrrvn0U~200d$=_ L2a=2-RF5G5Q$tJH diff --git a/events/index.js b/events/index.js index 80ee82e..a8c150f 100644 --- a/events/index.js +++ b/events/index.js @@ -2,9 +2,6 @@ import EventEmmiter from "events"; const globalEventEmmiter = new EventEmmiter(); -/** - * Start logging data from modbus slaves - */ globalEventEmmiter.on("log:start", (pollInterval) => {}); globalEventEmmiter.on("log:stop", () => {}); diff --git a/index.d.ts b/index.d.ts index 541d82e..93322e8 100644 --- a/index.d.ts +++ b/index.d.ts @@ -10,3 +10,23 @@ export type AppConfig = { log_interval_ms: number; }; +export type NumberType = 'UI16' | 'I16' | 'UI32' | 'I32' | 'FP32' + +export type DisplayValue = { + data?: number; + id: number; + name: string; + slave_id: number; + reg_addr: number; + reg_format: number; +} + +export type ModbusDevice = { + id: number; + name: string; + g_display_reg_addr: number | null; + g_display_reg_format: NumberType | null; + g_y_label: string | null; + is_logging: boolean; + display_values?: DisplayValue[] +} \ No newline at end of file diff --git a/migrations/001-init.sql b/migrations/001-init.sql index 822f187..6c843b6 100644 --- a/migrations/001-init.sql +++ b/migrations/001-init.sql @@ -21,10 +21,9 @@ CREATE TABLE IF NOT EXISTS "app_config" ( CREATE TABLE IF NOT EXISTS "modbus_slaves" ( "id" INTEGER CHECK (id >= 1 AND id <= 255), "name" VARCHAR(256) NOT NULL, - -- Holding register "g_display_reg_addr" INTEGER CHECK(g_display_reg_addr >= 0 AND g_display_reg_addr <= 65534) DEFAULT NULL, - -- Use 1 or 2 bytes - "g_display_reg_format" INTEGER CHECK(g_display_reg_format IN(16, 32)) DEFAULT 16, + "g_display_reg_type" VARCHAR(3) CHECK(g_display_reg_type, IN("HR", "IR")) DEFAULT "HR", + "g_display_reg_format" VARCHAR(5) CHECK(g_display_reg_format IN("UI16", "I16", "UI32", "I32", "FP32")) DEFAULT "UI16", "g_y_label" VARCHAR(40) DEFAULT "Label", "is_logging" BOOLEAN NOT NULL DEFAULT FALSE, PRIMARY KEY("id") @@ -58,9 +57,9 @@ CREATE TABLE IF NOT EXISTS "display_values" ( INSERT INTO "modbus_slaves" ( "id", "name", "g_display_reg_addr", "g_display_reg_format", "g_y_label", "is_logging" ) VALUES - (1, "Device #1", 0, 16, "Label 1", 1), - (2, "Device #2", 1, 32, "Label 2", 1), - (3, "Device #3", 3, 16, "Label 3", 0), + (1, "Device #1", 0, "UI16", "Label 1", 1), + (2, "Device #2", 1, "UI32", "Label 2", 1), + (3, "Device #3", 3, "UI16", "Label 3", 0), (4, "Device #4", NULL, NULL, "Label 4", 0); INSERT INTO "display_values" ( diff --git a/public/index.html b/public/index.html index 45531e9..de8fab7 100644 --- a/public/index.html +++ b/public/index.html @@ -28,7 +28,7 @@ No devices added. -
+
diff --git a/public/js/app.js b/public/js/app.js index ea55f0a..65b76de 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -5,8 +5,8 @@ import * as utils from "./utils/index.js"; var pages = ["loading", "monitoring", "configuration", "404"]; var dataStreamSource; -var graphData = [[0,0]]; -var g; +var graphData = []; +var g = null; document.addEventListener("alpine:init", async () => { Alpine.store("app", { @@ -62,11 +62,12 @@ document.addEventListener("alpine:init", async () => { isOpen: false, isLoading: false, error: null, + graphView: '', data: { id: null, name: "", g_display_reg_addr: null, - g_display_reg_format: 16, + g_display_reg_format: "UI16", g_y_label: "", is_logging: false, }, @@ -74,7 +75,7 @@ document.addEventListener("alpine:init", async () => { this.data.id = null; this.data.name = ""; this.data.g_display_reg_addr = null; - this.data.g_display_reg_format = 16; + this.data.g_display_reg_format = "UI16"; this.data.g_y_label = ""; this.data.is_logging = false; }, @@ -83,7 +84,6 @@ document.addEventListener("alpine:init", async () => { this.isLoading = true; this.error = null; await api.post("/modbus/create_device", utils.dellNullableKeys({ ...this.data })); - graphData = []; // todo: update graph data this.$dispatch("add-device", { ...this.data }); // add device to state this.resetDataFields(); this.close(); @@ -100,6 +100,8 @@ document.addEventListener("alpine:init", async () => { }, close() { this.isOpen = false; + this.resetDataFields(); + this.graphView = ''; }, })); @@ -111,6 +113,8 @@ document.addEventListener("alpine:init", async () => { resetGraph() { if (g instanceof Dygraph) { g.destroy(); + graphData = []; + g = null; } }, async init() { @@ -125,7 +129,8 @@ document.addEventListener("alpine:init", async () => { }, selectDevice(device) { if (device.id === this.selectedDevice?.id) return; - + + this.error = null; this.selectedDevice = device; this.resetGraph(); @@ -134,26 +139,29 @@ document.addEventListener("alpine:init", async () => { dataStreamSource?.close(); dataStreamSource = new EventSource(`/api/modbus/data_stream?slave_id=${device.id}`); - g = new Dygraph(document.getElementById("div_g"), graphData, { - drawPoints: false, - showRoller: false, - ylabel: device.g_y_label, - // valueRange: [0, 1000], - // labels: ["Time", "Random"], - }); - g.resize(720, 300); dataStreamSource.onerror = (e) => { dataStreamSource.close(); + this.isLoading = false; + this.error = { message: 'Something went wrong.' } } dataStreamSource.onmessage = (e) => { const data = JSON.parse(e.data); - console.log(data); - graphData.push([new Date(), data.graph.value]); - g.updateOptions({ file: graphData }); + + if (data.graph !== null) { + graphData.push([new Date(), data.graph.value]); + if (g === null) { + g = new Dygraph(document.getElementById("div_g"), graphData, { + drawPoints: false, + showRoller: false, + ylabel: device.g_y_label, + }); + g.resize(720, 300); + } + g.updateOptions({ file: graphData }); + } data.displayValues.forEach((v, i) => this.selectedDevice.display_values[i] = v); this.isLoading = false; }; - graphData = []; }, async removeDevice(id) { try { diff --git a/services/index.js b/services/index.js index 2a72f95..168655e 100644 --- a/services/index.js +++ b/services/index.js @@ -1 +1,2 @@ export * from "./app.service.js"; +export * from './modbus.service.js' \ No newline at end of file diff --git a/services/modbus.service.js b/services/modbus.service.js new file mode 100644 index 0000000..159ca46 --- /dev/null +++ b/services/modbus.service.js @@ -0,0 +1,39 @@ +import modbusClient from "#root/config/modbus-client.config.js"; +import * as utils from '#root/utils/index.js' + +export const modbusService = { + /** + * @param {import("..").ModbusDevice} device + * @param {"HR" | "IR"} type + */ + async readDataFromDevice(device, type = 'HR') { + const { g_display_reg_format: g_format, g_display_reg_addr: g_reg_addr } = device; + let readLength = 1; // 16 bits default + + modbusClient.setID(device.id); + + if (g_format?.includes('32')) readLength = 2; + if (g_format?.includes('64')) readLength = 4; + if (g_format?.includes('128')) readLength = 8; + + let graphValue; + if (type === 'HR') graphValue = await modbusClient.readHoldingRegisters(g_reg_addr, readLength); + if (type === 'IR') graphValue = await modbusClient.readInputRegisters(g_reg_addr, readLength); + + if (device.display_values) { + for (const value of device.display_values) { + value.data = Buffer.from((await modbusClient.readHoldingRegisters(value.reg_addr, 1)).buffer).readUInt16BE(); + } + } + + const graph = (g_reg_addr !== null ? { + value: utils.readNumberFromBuf(graphValue.buffer, device.g_display_reg_format, 'BE'), + format: g_format + } : null); + + return { + graph, + displayValues: device.display_values + } + } +} \ No newline at end of file diff --git a/utils/index.js b/utils/index.js new file mode 100644 index 0000000..23ae8fe --- /dev/null +++ b/utils/index.js @@ -0,0 +1 @@ +export * from './number.js' \ No newline at end of file diff --git a/utils/number.js b/utils/number.js new file mode 100644 index 0000000..d0bc699 --- /dev/null +++ b/utils/number.js @@ -0,0 +1,19 @@ +/** + * Read number from buffer + * @param {Buffer} buf + * @param {import("..").NumberType} type + * @param {"BE" | "LE"} order + */ +export function readNumberFromBuf(buf, type, order) { + /* Big-Endian */ + if (type === 'UI16' && order === 'BE') return Buffer.from(buf).readUInt16BE(); + if (type === 'I16' && order === 'BE') return Buffer.from(buf).readInt16BE(); + if (type === 'UI32' && order === 'BE') return Buffer.from(buf).readUint32BE(); + if (type === 'FP32' && order === 'BE') return Buffer.from(buf).readFloatBE(); + + /* Little-Endian */ + if (type === 'UI16' && order === 'LE') return Buffer.from(buf).readUInt16LE(); + if (type === 'I16' && order === 'LE') return Buffer.from(buf).readInt16LE(); + if (type === 'UI32' && order === 'LE') return Buffer.from(buf).readUint32LE(); + if (type === 'FP32' && order === 'LE') return Buffer.from(buf).readFloatLE(); +} \ No newline at end of file diff --git a/validators/modbus-device.schema.js b/validators/modbus-device.schema.js index 4e54c3c..bd40d83 100644 --- a/validators/modbus-device.schema.js +++ b/validators/modbus-device.schema.js @@ -4,7 +4,7 @@ export const modbusDeviceSchema = object({ id: number().required(), name: string().required(), g_display_reg_addr: number().min(0).max(65534).nullable(), - g_display_reg_format: number().oneOf([16, 32]).nullable(), + g_display_reg_format: string().oneOf(["UI16", "I16", "UI32", "I32", "FP32"]).nullable(), g_y_label: string().max(40).nullable(), is_logging: boolean().required() });