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 78ab51f..4549151 100644
Binary files a/data.db and b/data.db differ
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.
-
@@ -225,8 +228,8 @@
Value:
- Reg Addr:
- Format:
+ Reg Addr:
+ Format:
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()
});