This commit is contained in:
Sergey Yarkov 2023-10-30 21:22:54 +03:00
parent 90271dc45f
commit 6b9b15970d
No known key found for this signature in database
GPG Key ID: E91CE1FABC619B6F
3 changed files with 132 additions and 98 deletions

View File

@ -3,27 +3,27 @@ import modbusClient from "#root/config/modbus-client.config.js";
import { appService } from "#root/services/index.js"; import { appService } from "#root/services/index.js";
export const modbusController = { export const modbusController = {
/** /**
* Connect to modbus network using configuration from database * Connect to modbus network using configuration from database
* @param {import('express').Request} req * @param {import('express').Request} req
* @param {import('express').Response} res * @param {import('express').Response} res
* @param {import('express').NextFunction} next * @param {import('express').NextFunction} next
*/ */
async connect(req, res, next) { async connect(req, res, next) {
try { try {
const config = await appService.getConfig(db); const config = await appService.getConfig(db);
if (!config) return res.status(400).json({ message: "Application is not configured" }); if (!config) return res.status(400).json({ message: "Application is not configured" });
if (modbusClient.isOpen) { if (modbusClient.isOpen) {
modbusClient.close(undefined); modbusClient.close(undefined);
modbusClient.destroy(undefined); modbusClient.destroy(undefined);
} }
if (config.mb_connection_type === "TCP") { if (config.mb_connection_type === "TCP") {
await modbusClient.connectTCP(config.mb_tcp_ip, { port: config.mb_tcp_port }); await modbusClient.connectTCP(config.mb_tcp_ip, { port: config.mb_tcp_port });
return res.status(200).json({ message: "Connected." }); return res.status(200).json({ message: "Connected." });
} }
if (config.mb_connection_type === "RTU") { if (config.mb_connection_type === "RTU") {
await modbusClient.connectRTU(config.mb_rtu_path, { await modbusClient.connectRTU(config.mb_rtu_path, {
baudRate: config.mb_rtu_baud, baudRate: config.mb_rtu_baud,
@ -33,7 +33,7 @@ export const modbusController = {
}); });
return res.status(200).json({ message: "Connected." }); return res.status(200).json({ message: "Connected." });
} }
return res.status(400).json({ message: "Connection type is invalid." }); return res.status(400).json({ message: "Connection type is invalid." });
} catch (error) { } catch (error) {
next(error); next(error);
@ -42,16 +42,16 @@ export const modbusController = {
/** /**
* Close the modbus connection * Close the modbus connection
* @param {import('express').Request} req * @param {import('express').Request} req
* @param {import('express').Response} res * @param {import('express').Response} res
* @param {import('express').NextFunction} next * @param {import('express').NextFunction} next
*/ */
async close(req, res, next) { async close(req, res, next) {
try { try {
if (!modbusClient.isOpen) return res.status(400).json({ message: "Already closed." }); if (!modbusClient.isOpen) return res.status(400).json({ message: "Already closed." });
modbusClient.close(undefined); modbusClient.close(undefined);
modbusClient.destroy(undefined); modbusClient.destroy(undefined);
return res.status(200).json({ message: 'Closed.' }); return res.status(200).json({ message: "Closed." });
} catch (error) { } catch (error) {
next(error); next(error);
} }
@ -59,9 +59,9 @@ export const modbusController = {
/** /**
* Get list of modbus slave devices * Get list of modbus slave devices
* @param {import('express').Request} req * @param {import('express').Request} req
* @param {import('express').Response} res * @param {import('express').Response} res
* @param {import('express').NextFunction} next * @param {import('express').NextFunction} next
*/ */
async list(req, res, next) { async list(req, res, next) {
try { try {
@ -81,46 +81,34 @@ export const modbusController = {
LEFT JOIN display_values AS dv ON dv.slave_id = ms.id LEFT JOIN display_values AS dv ON dv.slave_id = ms.id
GROUP BY ms.id, ms.name; GROUP BY ms.id, ms.name;
`); `);
slaves.forEach(s => (s.display_values = JSON.parse(s.display_values))) slaves.forEach((s) => (s.display_values = JSON.parse(s.display_values)));
return res.status(200).json(slaves); return res.status(200).json(slaves);
} catch (error) { } catch (error) {
next(error) next(error);
} }
}, },
/** /**
* Create new modbus slave device * Create new modbus slave device
* @param {import('express').Request} req * @param {import('express').Request} req
* @param {import('express').Response} res * @param {import('express').Response} res
* @param {import('express').NextFunction} next * @param {import('express').NextFunction} next
*/ */
async createDevice(req, res, next) { async createDevice(req, res, next) {
try { try {
const { id, name, g_display_reg_addr, g_display_reg_format, g_y_label, is_logging } = req.body; const { id, name, g_display_reg_addr, g_display_reg_format, g_y_label, is_logging } = req.body;
await db.run(`INSERT INTO "modbus_slaves" ( await db.run(
`INSERT INTO "modbus_slaves" (
"id", "id",
"name", "name",
"g_display_reg_addr", "g_display_reg_addr",
"g_display_reg_format", "g_display_reg_format",
"g_y_label", "g_y_label",
"is_logging") "is_logging")
VALUES (?, ?, ?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?, ?)`,
[id, name, g_display_reg_addr, g_display_reg_format, g_y_label, is_logging]); [id, name, g_display_reg_addr, g_display_reg_format, g_y_label, is_logging],
return res.status(200).json({ message: 'Device created.' }); );
} catch (error) { return res.status(200).json({ message: "Device created." });
next(error)
}
},
/**
* Remove modbus slave device
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {import('express').NextFunction} next
*/
async updateDevice(req, res, next) {
try {
} catch (error) { } catch (error) {
next(error); next(error);
} }
@ -128,16 +116,29 @@ export const modbusController = {
/** /**
* Remove modbus slave device * Remove modbus slave device
* @param {import('express').Request} req * @param {import('express').Request} req
* @param {import('express').Response} res * @param {import('express').Response} res
* @param {import('express').NextFunction} next * @param {import('express').NextFunction} next
*/
async updateDevice(req, res, next) {
try {
} catch (error) {
next(error);
}
},
/**
* Remove modbus slave device
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {import('express').NextFunction} next
*/ */
async removeDevice(req, res, next) { async removeDevice(req, res, next) {
try { try {
const device = await db.get(`SELECT id FROM "modbus_slaves" WHERE id = ?`, [req.body.id]); const device = await db.get(`SELECT id FROM "modbus_slaves" WHERE id = ?`, [req.body.id]);
if (!device) return res.status(404).json({ message: 'Device not found.' }); if (!device) return res.status(404).json({ message: "Device not found." });
await db.run(`DELETE FROM "modbus_slaves" WHERE id = ?`, [req.body.id]); await db.run(`DELETE FROM "modbus_slaves" WHERE id = ?`, [req.body.id]);
return res.status(200).json({ message: 'Device removed.' }); return res.status(200).json({ message: "Device removed." });
} catch (error) { } catch (error) {
next(error); next(error);
} }
@ -145,22 +146,22 @@ export const modbusController = {
/** /**
* Create display value for modbus slave device * Create display value for modbus slave device
* @param {import('express').Request} req * @param {import('express').Request} req
* @param {import('express').Response} res * @param {import('express').Response} res
* @param {import('express').NextFunction} next * @param {import('express').NextFunction} next
*/ */
async createDisplayValue(req, res, next) {}, async createDisplayValue(req, res, next) {},
/** /**
* Remove display value from modbus slave device * Remove display value from modbus slave device
* @param {import('express').Request} req * @param {import('express').Request} req
* @param {import('express').Response} res * @param {import('express').Response} res
* @param {import('express').NextFunction} next * @param {import('express').NextFunction} next
*/ */
async removeDisplayValue(req, res, next) { async removeDisplayValue(req, res, next) {
try { try {
await db.run(`DELETE FROM "display_values" WHERE id = ?`, req.body.id); await db.run(`DELETE FROM "display_values" WHERE id = ?`, req.body.id);
return res.status(200).json({ message: 'Display value removed.' }); return res.status(200).json({ message: "Display value removed." });
} catch (error) { } catch (error) {
next(error); next(error);
} }
@ -168,17 +169,17 @@ export const modbusController = {
/** /**
* Get latest data log from modbus slave device * Get latest data log from modbus slave device
* @param {import('express').Request} req * @param {import('express').Request} req
* @param {import('express').Response} res * @param {import('express').Response} res
* @param {import('express').NextFunction} next * @param {import('express').NextFunction} next
*/ */
async getLatestLog(req, res, next) {}, async getLatestLog(req, res, next) {},
/** /**
* Creates event stream aboud modbus connection status * Creates event stream aboud modbus connection status
* @param {import('express').Request} req * @param {import('express').Request} req
* @param {import('express').Response} res * @param {import('express').Response} res
* @param {import('express').NextFunction} next * @param {import('express').NextFunction} next
*/ */
async status(req, res, next) { async status(req, res, next) {
try { try {
@ -188,7 +189,7 @@ export const modbusController = {
let intervalId = setInterval(() => { let intervalId = setInterval(() => {
res.write(`event: message\ndata: ${JSON.stringify(modbusClient.isOpen)}\n\n`); res.write(`event: message\ndata: ${JSON.stringify(modbusClient.isOpen)}\n\n`);
}, 500); }, 500);
req.on('close', () => clearInterval(intervalId)); req.on("close", () => clearInterval(intervalId));
} catch (error) { } catch (error) {
next(error); next(error);
} }
@ -196,27 +197,50 @@ export const modbusController = {
/** /**
* Creates event stream for sending data from modbus device * Creates event stream for sending data from modbus device
* @param {import('express').Request} req * @param {import('express').Request} req
* @param {import('express').Response} res * @param {import('express').Response} res
* @param {import('express').NextFunction} next * @param {import('express').NextFunction} next
*/ */
async streamData(req, res, next) { async streamData(req, res, next) {
try { try {
if (!modbusClient.isOpen) return res.status(503).json({ message: "Modbus connection closed." }); if (!modbusClient.isOpen) return res.status(503).json({ error: { message: "Modbus connection closed." } });
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]);
if (!device) return res.status(404).json({ error: { message: "Device not found." } });
res.setHeader("Cache-Control", "no-cache"); res.setHeader("Cache-Control", "no-cache");
res.setHeader("Content-Type", "text/event-stream"); res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Connection", "keep-alive"); res.setHeader("Connection", "keep-alive");
let interval = setInterval(async () => { let interval = setInterval(async () => {
const { data } = await modbusClient.readHoldingRegisters(0, 1); let value = 0;
res.write("event: messsage\n"); let len = 1; // 16 bits default
res.write(`data: ${JSON.stringify({ data })}`);
res.write("\n\n"); 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:
value = Buffer.from(result.buffer).readUInt32BE();
break;
case 16:
value = Buffer.from(result.buffer).readUint16BE();
break;
default:
break;
}
res.write("event: message\n");
res.write(`data: ${JSON.stringify({ graph: { value, format: device.g_display_reg_format } })}\n\n`);
}, 1000); }, 1000);
req.on("close", () => clearInterval(interval)); req.on("close", () => clearInterval(interval));
} catch (error) { } catch (error) {
next(error); next(error);
} }
} },
} };

BIN
data.db

Binary file not shown.

View File

@ -3,8 +3,10 @@ import "./alpine.js";
import api from "./api.js"; import api from "./api.js";
import * as utils from "./utils/index.js"; import * as utils from "./utils/index.js";
const pages = ["loading", "monitoring", "configuration", "404"]; var pages = ["loading", "monitoring", "configuration", "404"];
let graphData = [[0, 0]]; var dataStreamSource;
var graphData = [[0, 0]];
var g;
document.addEventListener("alpine:init", async () => { document.addEventListener("alpine:init", async () => {
Alpine.store("app", { Alpine.store("app", {
@ -56,38 +58,38 @@ document.addEventListener("alpine:init", async () => {
}, },
}); });
Alpine.data('deviceModal', () => ({ Alpine.data("deviceModal", () => ({
isOpen: false, isOpen: false,
isLoading: false, isLoading: false,
error: null, error: null,
data: { data: {
id: null, id: null,
name: '', name: "",
g_display_reg_addr: null, g_display_reg_addr: null,
g_display_reg_format: 16, g_display_reg_format: 16,
g_y_label: '', g_y_label: "",
is_logging: false is_logging: false,
}, },
resetDataFields() { resetDataFields() {
this.data.id = null; this.data.id = null;
this.data.name = ''; this.data.name = "";
this.data.g_display_reg_addr = null; this.data.g_display_reg_addr = null;
this.data.g_display_reg_format = 16; this.data.g_display_reg_format = 16;
this.data.g_y_label = ''; this.data.g_y_label = "";
this.data.is_logging = false; this.data.is_logging = false;
}, },
async create(cb) { async create(cb) {
try { try {
this.isLoading = true; this.isLoading = true;
this.error = null; this.error = null;
await api.post('/modbus/create_device', utils.dellNullableKeys({...this.data})); await api.post("/modbus/create_device", utils.dellNullableKeys({ ...this.data }));
graphData = [] // todo: update graph data graphData = []; // todo: update graph data
this.$dispatch('add-device', {...this.data}); // add device to state this.$dispatch("add-device", { ...this.data }); // add device to state
this.resetDataFields(); this.resetDataFields();
this.close(); this.close();
cb && cb(); cb && cb();
} catch (error) { } catch (error) {
this.error = error this.error = error;
console.error(error); console.error(error);
} finally { } finally {
this.isLoading = false; this.isLoading = false;
@ -99,7 +101,7 @@ document.addEventListener("alpine:init", async () => {
close() { close() {
this.isOpen = false; this.isOpen = false;
}, },
})) }));
Alpine.data("monitoringPage", () => ({ Alpine.data("monitoringPage", () => ({
selectedDevice: null, selectedDevice: null,
@ -107,18 +109,11 @@ document.addEventListener("alpine:init", async () => {
async init() { async init() {
try { try {
this.$store.app.currentPage = "loading"; this.$store.app.currentPage = "loading";
var g = new Dygraph(document.getElementById("div_g"), graphData, {
drawPoints: false,
showRoller: false,
ylabel: "Y Label",
valueRange: [0.0, 1.8],
// labels: ["Time", "Random"],
});
this.devices = await api.get("/modbus/devices"); this.devices = await api.get("/modbus/devices");
window.intervalId = setInterval(() => { // window.intervalId = setInterval(() => {
graphData.push([new Date(), Math.random()]); // graphData.push([new Date(), Math.random()]);
g.updateOptions({ file: graphData }); // g.updateOptions({ file: graphData });
}, 500); // }, 500);
} catch (error) { } catch (error) {
this.$store.app.error = error; this.$store.app.error = error;
} finally { } finally {
@ -126,6 +121,21 @@ document.addEventListener("alpine:init", async () => {
} }
}, },
selectDevice(device) { selectDevice(device) {
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"],
});
dataStreamSource.onmessage = (e) => {
const data = JSON.parse(e.data);
console.log(data);
graphData.push([new Date(), data.graph.value]);
g.updateOptions({ file: graphData });
};
if (device.id === this.selectedDevice?.id) return; if (device.id === this.selectedDevice?.id) return;
graphData = []; // todo: reset graph graphData = []; // todo: reset graph
this.selectedDevice = device; this.selectedDevice = device;
@ -141,12 +151,12 @@ document.addEventListener("alpine:init", async () => {
}, },
async removeDisplayValue(id) { async removeDisplayValue(id) {
try { try {
await api.post('/modbus/remove_display-value', { id }); await api.post("/modbus/remove_display-value", { id });
this.selectedDevice.display_values = this.selectedDevice.display_values.filter(v => v.id != id); this.selectedDevice.display_values = this.selectedDevice.display_values.filter((v) => v.id != id);
} catch (error) { } catch (error) {
console.error("Error while removing display value", error); console.error("Error while removing display value", error);
} }
} },
})); }));
Alpine.data("configAppPage", () => ({ Alpine.data("configAppPage", () => ({