From 8932402d3b314f81f08860c1835a19f8508596a2 Mon Sep 17 00:00:00 2001 From: sergeyyarkov Date: Thu, 26 Oct 2023 17:41:52 +0300 Subject: [PATCH] progress --- controllers/modbus.controller.js | 33 ++- data.db | Bin 20480 -> 20480 bytes index.d.ts | 5 +- migrations/001-init.sql | 15 +- public/css/dist.css | 160 +++++++++- public/css/main.css | 8 + public/index.html | 491 +++++++++++++++++-------------- public/js/app.js | 97 ++++-- public/js/utils/index.js | 4 +- routes/api.router.js | 4 +- scripts/db-init.script.js | 2 +- validators/index.js | 2 + 12 files changed, 555 insertions(+), 266 deletions(-) diff --git a/controllers/modbus.controller.js b/controllers/modbus.controller.js index e25fe86..6bb9f09 100644 --- a/controllers/modbus.controller.js +++ b/controllers/modbus.controller.js @@ -85,7 +85,16 @@ export const modbusController = { * @param {import('express').Response} res * @param {import('express').NextFunction} next */ - async removeSlaveDevice(req, res, next) {}, + async removeSlaveDevice(req, res, next) { + try { + 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.' }); + await db.run(`DELETE FROM "modbus_slaves" WHERE id = ?`, [req.body.id]); + return res.status(200).json({ message: 'Device removed.' }); + } catch (error) { + next(error); + } + }, /** * Create display value for modbus slave device @@ -112,7 +121,27 @@ export const modbusController = { async getLatestLog(req, res, next) {}, /** - * Create event stream for sending data from modbus device + * Creates event stream aboud modbus connection status + * @param {import('express').Request} req + * @param {import('express').Response} res + * @param {import('express').NextFunction} next + */ + async status(req, res, next) { + try { + res.setHeader("Cache-Control", "no-cache"); + res.setHeader("Content-Type", "text/event-stream"); + res.setHeader("Connection", "keep-alive"); + let intervalId = setInterval(() => { + res.write(`event: message\ndata: ${JSON.stringify(modbusClient.isOpen)}\n\n`); + }, 500); + req.on('close', () => clearInterval(intervalId)); + } catch (error) { + next(error); + } + }, + + /** + * Creates event stream for sending data from modbus device * @param {import('express').Request} req * @param {import('express').Response} res * @param {import('express').NextFunction} next diff --git a/data.db b/data.db index c7fe9acfbfb0a564f56f86485d2f4d8603fec842..3027c9172cb245eddaa422b7c3365acdca01d27f 100644 GIT binary patch delta 752 zcmZozz}T>Wae}nqO$G)ARv?Cfx`{f*qBj}z(q8fcg;+Qf8Tcc4(zsS|CT?te%dvSg zr#mB8vo$ljxVk!Ho9Sd0?tV#KT?LQ)oRrMGbcLeS^vvRt)FNIkPNnqt$y~AuOh8IY zRtiL&hZd(Q7%Jo!DHtgvRhFa{L)E|KRst#H=TR0-kFSi+NlZ%3QBnwV z4085x4AL+$*sQ~IlbMyhjDfvuV`C0G$3%8Ub_UV;&YS;8@Uqa+9g}r>a zPzh+ip{5Q|VgNDG1WBO@P}ncj#|K72 zjWyu{E4Sw5;^mqwC_iuWTKOb_&B6v}`6u7e*Wh8{pU%MljQ=$M2L9=r1r^%)GsRih z8RQulq&Z-oGJ&|yM3NaO!N`b^Fy;_|NEu5qL8LfgdW@J9puUm-CI@*=MrpW|Ayktg E02=|p9RL6T delta 253 zcmZozz}T>Wae}m99RmXcD-gp#)I=R)(K-gbw3oaJCQt~O8#RWNumGMQX>G6pvDMbo4whCsZ zrp6|kI=oz*N|P^eD^0eOQQmCCbD5cy;|l}F7oe%HI5_$^8QB>`XS#2$6X#_i-Mq<0 pJSLkr$QUzD7L=d2d98es04vB(%s|6W^G}?h$OiHYg8-DP4**>VL8t%# diff --git a/index.d.ts b/index.d.ts index 94acd34..257292a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -10,9 +10,10 @@ export type AppConfig = { log_interval_ms: number; }; -export type ModbusSlave = { +export type SlaveModbusDevice = { id: number; name: string; - display_addr: number; + display_reg_addr: number | null; + display_reg_format: number | null; is_logging: boolean; } \ No newline at end of file diff --git a/migrations/001-init.sql b/migrations/001-init.sql index beee752..0d1c201 100644 --- a/migrations/001-init.sql +++ b/migrations/001-init.sql @@ -21,8 +21,11 @@ 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, - "display_reg_addr" INTEGER CHECK(display_reg_addr >= 0 AND display_reg_addr <= 65534), - "display_reg_format" INTEGER CHECK(display_reg_format IN(16, 32)), + -- Holding register + "g_display_reg_addr" INTEGER CHECK(g_display_reg_addr >= 0 AND g_display_reg_addr <= 65534), + -- Use 1 or 2 bytes + "g_display_reg_format" INTEGER CHECK(g_display_reg_format IN(16, 32)), + "g_y_label" VARCHAR(40), "is_logging" BOOLEAN NOT NULL, PRIMARY KEY("id") ); @@ -36,6 +39,14 @@ CREATE TABLE IF NOT EXISTS "display_values" ( FOREIGN KEY("slave_id") REFERENCES modbus_slaves("id") ON DELETE CASCADE ); +INSERT INTO "modbus_slaves" ( + "id", "name", "g_display_reg_addr", "g_display_reg_format", "g_y_label", "is_logging" +) VALUES + (1, "Device #1", 1, 16, "Label 1", 1), + (2, "Device #2", 2, 32, "Label 2", 1), + (3, "Device #3", 8, 16, "Label 3", 0), + (4, "Device #4", NULL, NULL, "Label 4", 0); + -------------------------------------------------------------------------------- -- Down -------------------------------------------------------------------------------- diff --git a/public/css/dist.css b/public/css/dist.css index 9b6b569..9bb17d3 100644 --- a/public/css/dist.css +++ b/public/css/dist.css @@ -603,19 +603,27 @@ video { position: relative; } +.z-50 { + z-index: 50; +} + .mx-auto { margin-left: auto; margin-right: auto; } +.my-2 { + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} + .my-4 { margin-top: 1rem; margin-bottom: 1rem; } -.my-2 { - margin-top: 0.5rem; - margin-bottom: 0.5rem; +.mb-3 { + margin-bottom: 0.75rem; } .ml-auto { @@ -630,6 +638,22 @@ video { margin-right: 0.5rem; } +.mr-auto { + margin-right: auto; +} + +.mt-1 { + margin-top: 0.25rem; +} + +.mt-1\.5 { + margin-top: 0.375rem; +} + +.mt-10 { + margin-top: 2.5rem; +} + .mt-16 { margin-top: 4rem; } @@ -638,6 +662,10 @@ video { margin-top: 0.5rem; } +.mt-20 { + margin-top: 5rem; +} + .mt-3 { margin-top: 0.75rem; } @@ -646,8 +674,12 @@ video { margin-top: 1rem; } -.mt-10 { - margin-top: 2.5rem; +.mt-5 { + margin-top: 1.25rem; +} + +.mt-6 { + margin-top: 1.5rem; } .block { @@ -706,14 +738,14 @@ video { max-width: 42rem; } -.max-w-6xl { - max-width: 72rem; -} - .max-w-5xl { max-width: 64rem; } +.max-w-md { + max-width: 28rem; +} + .flex-1 { flex: 1 1 0%; } @@ -762,6 +794,10 @@ video { align-items: center; } +.justify-end { + justify-content: flex-end; +} + .justify-between { justify-content: space-between; } @@ -772,6 +808,12 @@ video { margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); } +.space-y-1 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0.25rem * var(--tw-space-y-reverse)); +} + .space-y-2 > :not([hidden]) ~ :not([hidden]) { --tw-space-y-reverse: 0; margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse))); @@ -816,6 +858,21 @@ video { border-style: dashed; } +.bg-gray-100 { + --tw-bg-opacity: 1; + background-color: rgb(243 244 246 / var(--tw-bg-opacity)); +} + +.bg-red-500 { + --tw-bg-opacity: 1; + background-color: rgb(239 68 68 / var(--tw-bg-opacity)); +} + +.bg-white { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} + .fill-blue-600 { fill: #2563eb; } @@ -862,15 +919,37 @@ video { padding-right: 0.625rem; } +.px-4 { + padding-left: 1rem; + padding-right: 1rem; +} + .py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; } +.py-3\.5 { + padding-top: 0.875rem; + padding-bottom: 0.875rem; +} + +.pl-2 { + padding-left: 0.5rem; +} + +.pr-6 { + padding-right: 1.5rem; +} + .pt-5 { padding-top: 1.25rem; } +.text-left { + text-align: left; +} + .text-center { text-align: center; } @@ -885,9 +964,14 @@ video { line-height: 2.25rem; } -.text-5xl { - font-size: 3rem; - line-height: 1; +.text-4xl { + font-size: 2.25rem; + line-height: 2.5rem; +} + +.text-lg { + font-size: 1.125rem; + line-height: 1.75rem; } .text-sm { @@ -895,6 +979,11 @@ video { line-height: 1.25rem; } +.text-xl { + font-size: 1.25rem; + line-height: 1.75rem; +} + .font-bold { font-weight: 700; } @@ -913,16 +1002,40 @@ video { color: rgb(156 163 175 / var(--tw-text-opacity)); } +.text-gray-500 { + --tw-text-opacity: 1; + color: rgb(107 114 128 / var(--tw-text-opacity)); +} + .text-gray-700 { --tw-text-opacity: 1; color: rgb(55 65 81 / var(--tw-text-opacity)); } +.text-green-700 { + --tw-text-opacity: 1; + color: rgb(21 128 61 / var(--tw-text-opacity)); +} + +.text-red-500 { + --tw-text-opacity: 1; + color: rgb(239 68 68 / var(--tw-text-opacity)); +} + .text-red-600 { --tw-text-opacity: 1; color: rgb(220 38 38 / var(--tw-text-opacity)); } +.text-red-700 { + --tw-text-opacity: 1; + color: rgb(185 28 28 / var(--tw-text-opacity)); +} + +.underline { + text-decoration-line: underline; +} + .opacity-70 { opacity: 0.7; } @@ -943,6 +1056,24 @@ video { display: none !important; } +.backdrop { + position: fixed; + top: 0px; + left: 0px; + bottom: 0px; + right: 0px; + overflow-y: auto; + background-color: rgba(0,0,0,.15); + padding-left: 0.625rem; + padding-right: 0.625rem; +} + +a { + --tw-text-opacity: 1; + color: rgb(37 99 235 / var(--tw-text-opacity)); + text-decoration-line: underline; +} + input, select { margin-top: 0.125rem; @@ -971,6 +1102,11 @@ button:disabled { pointer-events: all !important; } +.hover\:bg-gray-100:hover { + --tw-bg-opacity: 1; + background-color: rgb(243 244 246 / var(--tw-bg-opacity)); +} + .hover\:bg-gray-50:hover { --tw-bg-opacity: 1; background-color: rgb(249 250 251 / var(--tw-bg-opacity)); diff --git a/public/css/main.css b/public/css/main.css index 40caec4..a9f2852 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -12,6 +12,14 @@ display: none !important; } +.backdrop { + @apply fixed top-0 left-0 bottom-0 right-0 bg-[rgba(0,0,0,.15)] px-2.5 overflow-y-auto; +} + +a { + @apply underline text-blue-600; +} + input, select { @apply w-full border py-1 rounded-sm my-0.5 bg-white; diff --git a/public/index.html b/public/index.html index f7909db..e1c06ac 100644 --- a/public/index.html +++ b/public/index.html @@ -5,6 +5,7 @@ Modbus Logger App + @@ -17,243 +18,289 @@
-
- -
-
-
- Devices list: -
-
-

Device #1

+ + + + -
-
-
- + -
-
-

Something went wrong.

-

Refresh page to try again.

-
-
-

+      
 
       
-      
-
-

Page not found

+ + +
+ -
+
diff --git a/public/js/app.js b/public/js/app.js index 9ce0a32..157ff87 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -4,34 +4,18 @@ import api from "./api.js"; import * as utils from "./utils/index.js"; const pages = ["loading", "monitoring", "configuration", "404"]; - -var data = []; -var t = new Date(); +let graphData = []; +const t = new Date(); for (var i = 10; i >= 0; i--) { var x = new Date(t.getTime() - i * 1000); - data.push([x, Math.random()]); + graphData.push([x, Math.random()]); } -document.addEventListener("DOMContentLoaded", () => { - var g = new Dygraph(document.getElementById("div_g"), data, { - drawPoints: false, - showRoller: false, - // valueRange: [0.0, 6.8], - labels: ["Time", "Random"], - }); - - window.intervalId = setInterval(function () { - var x = new Date(); // current time - var y = Math.random(); - data.push([x, y]); - g.updateOptions({ file: data }); - }, 1000); -}); - document.addEventListener("alpine:init", async () => { Alpine.store("app", { is_configured: false, + is_opened: false, config: { mb_connection_type: "TCP", mb_tcp_ip: "127.0.0.1", @@ -45,9 +29,9 @@ document.addEventListener("alpine:init", async () => { }, currentPage: "loading", error: null, - devices: [], async init() { try { + const connStatusEventSource = new EventSource('/api/modbus/status'); const { config } = await api.get("/app/config"); const { is_configured } = await api.get("/app/is_configured"); @@ -55,14 +39,15 @@ document.addEventListener("alpine:init", async () => { if (config) this.config = config; if (is_configured) { - const devices = await api.get("/modbus/devices"); - this.devices = devices; + connStatusEventSource.onmessage = (e) => (this.is_opened = e.data === 'true'); + await utils.wait(500); this.currentPage = "monitoring"; return; } this.currentPage = "configuration"; } catch (error) { + console.error(error); this.currentPage = "error"; this.error = error; } @@ -78,6 +63,46 @@ document.addEventListener("alpine:init", async () => { }, }); + Alpine.data('monitoringPage', () => ({ + selectedDevice: null, + devices: [], + async init() { + try { + 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'); + window.intervalId = setInterval(() => { + graphData.push([new Date(), Math.random()]); + g.updateOptions({ file: graphData }) + }, 500) + } catch (error) { + this.$store.app.error = error; + } finally { + this.$store.app.currentPage = 'monitoring' + } + }, + selectDevice(device) { + if (device.id === this.selectedDevice?.id) return; + graphData = []; + this.selectedDevice = device; + }, + async removeDevice(id, ctx) { + try { + //await api.post('/modbus/remove_device', { id }); + this.devices = _this.devices.filter(d => d.id != id); + this.selectedDevice = null; + } catch (error) { + console.error('Error while removing device.', error) + } + } + })) + Alpine.data("configAppPage", () => ({ selectedConnectionType: "TCP", isLoading: false, @@ -99,6 +124,7 @@ document.addEventListener("alpine:init", async () => { }; await api.patch("/app/config", body[data.mb_connection_type]); await api.post("/modbus/connect"); + await utils.wait(500); this.$store.app.currentPage = "monitoring"; } catch (error) { this.errorMessage = `${error.message}`; @@ -107,4 +133,29 @@ document.addEventListener("alpine:init", async () => { } }, })); + + Alpine.store("confirmDialog", { + isOpen: false, + labels: { + title: 'Confirm dialog', + body: 'Are you sure you want to perform this action?', + }, + cb: { + fn: null, + args: [] + }, + open(title, body, cb, ...args) { + this.isOpen = true; + this.labels = { title, body }; + this.cb.fn = cb; + this.cb.args = args; + }, + confirm() { + this.cb.fn && this.cb.fn(...this.cb.args); + this.close(); + }, + close() { + this.isOpen = false; + } + }) }); diff --git a/public/js/utils/index.js b/public/js/utils/index.js index f20a3dd..22438a0 100644 --- a/public/js/utils/index.js +++ b/public/js/utils/index.js @@ -1 +1,3 @@ -export * from './object.js' \ No newline at end of file +export * from './object.js' + +export const wait = (ms) => new Promise((res) => setTimeout(() => res(), ms)); \ No newline at end of file diff --git a/routes/api.router.js b/routes/api.router.js index de6e288..611a3a5 100644 --- a/routes/api.router.js +++ b/routes/api.router.js @@ -3,7 +3,7 @@ import express from "express"; /** * Validators */ -import { appConfigSchema } from "#root/validators/index.js"; +import { appConfigSchema, removeModbusDeviceSchema } from "#root/validators/index.js"; /** * Controllers @@ -22,8 +22,10 @@ apiRouter.patch("/app/config", validateBodyMiddleware(appConfigSchema), appContr apiRouter.get("/app/is_configured", appController.isConfigured); apiRouter.get("/modbus/devices", modbusController.list); +apiRouter.post('/modbus/remove_device', validateBodyMiddleware(removeModbusDeviceSchema), modbusController.removeSlaveDevice); apiRouter.post("/modbus/connect", modbusController.connect); apiRouter.post("/modbus/close", modbusController.close); apiRouter.get("/modbus/data_stream", modbusController.streamData); +apiRouter.get('/modbus/status', modbusController.status); export { apiRouter }; diff --git a/scripts/db-init.script.js b/scripts/db-init.script.js index 0e47190..f00b477 100644 --- a/scripts/db-init.script.js +++ b/scripts/db-init.script.js @@ -2,7 +2,7 @@ import db from "#root/config/database.config.js"; async function run() { try { - await db.migrate({ force: false }); + await db.migrate({ force: true }); console.log("Database initialized."); } catch (error) { console.error(error); diff --git a/validators/index.js b/validators/index.js index b99fe8f..0a195cf 100644 --- a/validators/index.js +++ b/validators/index.js @@ -16,3 +16,5 @@ export const appConfigSchema = object({ mb_rtu_stop_bits: number().oneOf([1, 2]).when("mb_connection_type", requiredIfConnTypeEquals("RTU")), log_interval_ms: number().min(1000).required(), }); + +export const removeModbusDeviceSchema = object({ id: number().max(255).min(1).required() }) \ No newline at end of file