This commit is contained in:
Sergey Yarkov 2023-10-25 21:35:19 +03:00
parent 8e1481016e
commit 1cdf164fba
No known key found for this signature in database
GPG Key ID: E91CE1FABC619B6F
8 changed files with 181 additions and 74 deletions

BIN
data.db

Binary file not shown.

View File

@ -13,9 +13,12 @@ export function validateBodyMiddleware(schema) {
await schema.validate(req.body); await schema.validate(req.body);
next(); next();
} catch (error) { } catch (error) {
return res.status(400).json({ error: { return res.status(400).json({
message: error.errors[0] error: {
}}); status: 400,
message: error.errors[0],
},
});
} }
}; };
} }

View File

@ -21,8 +21,8 @@ CREATE TABLE IF NOT EXISTS "app_config" (
CREATE TABLE IF NOT EXISTS "modbus_slaves" ( CREATE TABLE IF NOT EXISTS "modbus_slaves" (
"id" INTEGER CHECK (id >= 1 AND id <= 255), "id" INTEGER CHECK (id >= 1 AND id <= 255),
"name" VARCHAR(256) NOT NULL, "name" VARCHAR(256) NOT NULL,
"display_addr" INTEGER CHECK(display_addr >= 0 AND display_addr <= 65534), "display_reg_addr" INTEGER CHECK(display_reg_addr >= 0 AND display_reg_addr <= 65534),
-- "display_val_format" INTEGER CHECK IN(16, 32) NOT NULL, "display_reg_format" INTEGER CHECK(display_reg_format IN(16, 32)),
"is_logging" BOOLEAN NOT NULL, "is_logging" BOOLEAN NOT NULL,
PRIMARY KEY("id") PRIMARY KEY("id")
); );

View File

@ -603,15 +603,21 @@ video {
position: relative; position: relative;
} }
.m-2 {
margin: 0.5rem;
}
.mx-auto { .mx-auto {
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
} }
.my-4 {
margin-top: 1rem;
margin-bottom: 1rem;
}
.my-2 {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.ml-auto { .ml-auto {
margin-left: auto; margin-left: auto;
} }
@ -640,6 +646,10 @@ video {
margin-top: 1rem; margin-top: 1rem;
} }
.mt-10 {
margin-top: 2.5rem;
}
.block { .block {
display: block; display: block;
} }
@ -664,10 +674,18 @@ video {
height: 5rem; height: 5rem;
} }
.h-full {
height: 100%;
}
.h-screen { .h-screen {
height: 100vh; height: 100vh;
} }
.max-h-\[520px\] {
max-height: 520px;
}
.w-1 { .w-1 {
width: 0.25rem; width: 0.25rem;
} }
@ -676,32 +694,12 @@ video {
width: 5rem; width: 5rem;
} }
.w-48 {
width: 12rem;
}
.w-72 {
width: 18rem;
}
.w-full { .w-full {
width: 100%; width: 100%;
} }
.w-24 { .min-w-\[256px\] {
width: 6rem; min-width: 256px;
}
.w-16 {
width: 4rem;
}
.w-12 {
width: 3rem;
}
.w-64 {
width: 16rem;
} }
.max-w-2xl { .max-w-2xl {
@ -712,6 +710,14 @@ video {
max-width: 72rem; max-width: 72rem;
} }
.max-w-5xl {
max-width: 64rem;
}
.flex-1 {
flex: 1 1 0%;
}
.flex-auto { .flex-auto {
flex: 1 1 auto; flex: 1 1 auto;
} }
@ -730,6 +736,16 @@ video {
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
} }
.cursor-pointer {
cursor: pointer;
}
.select-none {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.resize { .resize {
resize: both; resize: both;
} }
@ -756,12 +772,22 @@ video {
margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-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)));
margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
}
.space-y-3 > :not([hidden]) ~ :not([hidden]) { .space-y-3 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0; --tw-space-y-reverse: 0;
margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse))); margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(0.75rem * var(--tw-space-y-reverse)); margin-bottom: calc(0.75rem * var(--tw-space-y-reverse));
} }
.overflow-y-auto {
overflow-y: auto;
}
.rounded-sm { .rounded-sm {
border-radius: 0.125rem; border-radius: 0.125rem;
} }
@ -770,6 +796,14 @@ video {
border-width: 1px; border-width: 1px;
} }
.border-2 {
border-width: 2px;
}
.border-b {
border-bottom-width: 1px;
}
.border-l { .border-l {
border-left-width: 1px; border-left-width: 1px;
} }
@ -778,6 +812,10 @@ video {
border-right-width: 1px; border-right-width: 1px;
} }
.border-dashed {
border-style: dashed;
}
.fill-blue-600 { .fill-blue-600 {
fill: #2563eb; fill: #2563eb;
} }
@ -786,6 +824,14 @@ video {
padding: 2.5rem; padding: 2.5rem;
} }
.p-2 {
padding: 0.5rem;
}
.p-2\.5 {
padding: 0.625rem;
}
.p-20 { .p-20 {
padding: 5rem; padding: 5rem;
} }
@ -816,19 +862,13 @@ video {
padding-right: 0.625rem; padding-right: 0.625rem;
} }
.py-1 { .py-3 {
padding-top: 0.25rem; padding-top: 0.75rem;
padding-bottom: 0.25rem; padding-bottom: 0.75rem;
} }
.py-1\.5 { .pt-5 {
padding-top: 0.375rem; padding-top: 1.25rem;
padding-bottom: 0.375rem;
}
.py-8 {
padding-top: 2rem;
padding-bottom: 2rem;
} }
.text-center { .text-center {
@ -845,6 +885,11 @@ video {
line-height: 2.25rem; line-height: 2.25rem;
} }
.text-5xl {
font-size: 3rem;
line-height: 1;
}
.text-sm { .text-sm {
font-size: 0.875rem; font-size: 0.875rem;
line-height: 1.25rem; line-height: 1.25rem;
@ -863,6 +908,11 @@ video {
color: rgb(229 231 235 / var(--tw-text-opacity)); color: rgb(229 231 235 / var(--tw-text-opacity));
} }
.text-gray-400 {
--tw-text-opacity: 1;
color: rgb(156 163 175 / var(--tw-text-opacity));
}
.text-gray-700 { .text-gray-700 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(55 65 81 / var(--tw-text-opacity)); color: rgb(55 65 81 / var(--tw-text-opacity));
@ -921,6 +971,11 @@ button:disabled {
pointer-events: all !important; pointer-events: all !important;
} }
.hover\:bg-gray-50:hover {
--tw-bg-opacity: 1;
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
}
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
.dark\:text-gray-600 { .dark\:text-gray-600 {
--tw-text-opacity: 1; --tw-text-opacity: 1;

View File

@ -5,34 +5,55 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Modbus Logger App</title> <title>Modbus Logger App</title>
<link rel="stylesheet" href="/css/dist.css" /> <link rel="stylesheet" href="/css/dist.css" />
<script defer src="/js/dygraphs.min.js"></script>
<script defer type="module" src="/js/app.js"></script> <script defer type="module" src="/js/app.js"></script>
</head> </head>
<body class="m-2"> <body>
<!-- <div class="border p-4" x-data> <!-- <div class="border p-4" x-data>
<span>app state:</span> <span>app state:</span>
<pre x-text="JSON.stringify($store.app, undefined, 2)"></pre> <pre x-text="JSON.stringify($store.app, undefined, 2)"></pre>
</div> --> </div> -->
<!-- Root element --> <!-- Root element -->
<div x-data x-cloak class="mx-auto max-w-6xl m-2 py-8"> <div x-data x-cloak class="mx-auto max-w-5xl flex flex-col h-screen">
<!-- Monitoring content --> <!-- Monitoring content -->
<main x-bind:hidden="$store.app.currentPage !== 'monitoring'"> <main class="flex-1" x-bind:hidden="$store.app.currentPage !== 'monitoring'">
<!-- <div> <!-- <div>
<h1 class="text-3xl font-bold">Monitoring page</h1> <h1 class="text-3xl font-bold">Monitoring page</h1>
<button @click="$store.app.setPage('configuration')" class="btn">Config</button> <button @click="$store.app.setPage('configuration')" class="btn">Config</button>
</div> --> </div> -->
<div class="flex flex-auto flex-row h-screen space-x-2"> <div class="flex h-full flex-row space-x-2">
<div class="border-r w-64 p-4"> <div class="border-r min-w-[256px] p-2.5 pt-5">
<div class="w-full border px-2.5 py-1.5 rounded-sm"> <div class="flex flex-col space-y-2">
<h2 class="text-sm">Modbus slave #1</h2> <span class="italic text-sm">Devices list:</span>
<div class="flex flex-col max-h-[520px] overflow-y-auto space-y-2">
<div class="w-full select-none border px-2.5 py-3 rounded-sm hover:bg-gray-50 cursor-pointer">
<h2 class="text-md font-bold">Device #1</h2>
</div>
<div class="w-full select-none border px-2.5 py-3 rounded-sm hover:bg-gray-50 cursor-pointer">
<h2 class="text-md font-bold">Device #2</h2>
</div>
<div class="w-full select-none border px-2.5 py-3 rounded-sm hover:bg-gray-50 cursor-pointer">
<h2 class="text-md font-bold">Device #3</h2>
</div>
</div>
<div class="w-full text-center border-2 border-dashed rounded-sm hover:bg-gray-50 cursor-pointer">
<span class="text-5xl text-gray-400 select-none">+</span>
</div>
</div> </div>
<hr class="my-4" />
<div><span class="italic text-sm">Connection:</span><br /></div>
</div> </div>
<div class="w-full"> <div class="flex-auto">
graph... <div class="pt-5 border-b">
</div> <h2 class="font-bold text-3xl my-2">Device #1</h2>
<div class="border-l w-48 p-4"> </div>
<div class="mt-4">
<div id="div_g" style="width: 720px; height: 300px"></div>
</div>
<hr class="mt-10" />
</div> </div>
<!-- <div class="border-l"></div> -->
</div> </div>
</main> </main>
@ -222,8 +243,7 @@
<p>Refresh page to try again.</p> <p>Refresh page to try again.</p>
<br /> <br />
<div class="border p-4"> <div class="border p-4">
<pre x-text="JSON.stringify($store.app.error, undefined, 2)" x-show="$store.app.error"> <pre x-text="JSON.stringify($store.app.error, undefined, 2)" x-show="$store.app.error"></pre>
</pre>
</div> </div>
</div> </div>
</main> </main>

View File

@ -1,9 +1,33 @@
// @ts-nocheck // @ts-nocheck
import "./alpine.js"; 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'] const pages = ["loading", "monitoring", "configuration", "404"];
var data = [];
var t = new Date();
for (var i = 10; i >= 0; i--) {
var x = new Date(t.getTime() - i * 1000);
data.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 () => { document.addEventListener("alpine:init", async () => {
Alpine.store("app", { Alpine.store("app", {
@ -31,7 +55,7 @@ document.addEventListener("alpine:init", async () => {
if (config) this.config = config; if (config) this.config = config;
if (is_configured) { if (is_configured) {
const devices = await api.get('/modbus/devices'); const devices = await api.get("/modbus/devices");
this.devices = devices; this.devices = devices;
this.currentPage = "monitoring"; this.currentPage = "monitoring";
return; return;
@ -39,19 +63,19 @@ document.addEventListener("alpine:init", async () => {
this.currentPage = "configuration"; this.currentPage = "configuration";
} catch (error) { } catch (error) {
this.currentPage = 'error' this.currentPage = "error";
this.error = error; this.error = error;
} }
}, },
setPage(page) { setPage(page) {
if (!pages.includes(page)) { if (!pages.includes(page)) {
this.currentPage = '404' this.currentPage = "404";
return; return;
} }
this.currentPage = page; this.currentPage = page;
} },
}); });
Alpine.data("configAppPage", () => ({ Alpine.data("configAppPage", () => ({
@ -64,11 +88,17 @@ document.addEventListener("alpine:init", async () => {
this.errorMessage = ""; this.errorMessage = "";
const data = { ...this.$store.app.config }; const data = { ...this.$store.app.config };
const body = { const body = {
RTU: utils.objKeysExclude(data, ['mb_tcp_ip', 'mb_tcp_port']), RTU: utils.objKeysExclude(data, ["mb_tcp_ip", "mb_tcp_port"]),
TCP: utils.objKeysExclude(data, ['mb_rtu_path', 'mb_rtu_baud', 'mb_rtu_parity', 'mb_rtu_data_bits', 'mb_rtu_stop_bits']) TCP: utils.objKeysExclude(data, [
} "mb_rtu_path",
"mb_rtu_baud",
"mb_rtu_parity",
"mb_rtu_data_bits",
"mb_rtu_stop_bits",
]),
};
await api.patch("/app/config", body[data.mb_connection_type]); await api.patch("/app/config", body[data.mb_connection_type]);
await api.post("/modbus/connect"); await api.post("/modbus/connect");
this.$store.app.currentPage = "monitoring"; this.$store.app.currentPage = "monitoring";
} catch (error) { } catch (error) {
this.errorMessage = `${error.message}`; this.errorMessage = `${error.message}`;

View File

@ -1,5 +1,4 @@
import express from "express"; import express from "express";
import modbusClient from "#root/config/modbus-client.config.js";
/** /**
* Validators * Validators
@ -9,7 +8,7 @@ import { appConfigSchema } from "#root/validators/index.js";
/** /**
* Controllers * Controllers
*/ */
import { appController, modbusController } from '#root/controllers/index.js' import { appController, modbusController } from "#root/controllers/index.js";
/** /**
* Middlewares * Middlewares
@ -22,7 +21,7 @@ apiRouter.get("/app/config", appController.getConfig);
apiRouter.patch("/app/config", validateBodyMiddleware(appConfigSchema), appController.updateConfig); apiRouter.patch("/app/config", validateBodyMiddleware(appConfigSchema), appController.updateConfig);
apiRouter.get("/app/is_configured", appController.isConfigured); apiRouter.get("/app/is_configured", appController.isConfigured);
apiRouter.get('/modbus/devices', modbusController.list); apiRouter.get("/modbus/devices", modbusController.list);
apiRouter.post("/modbus/connect", modbusController.connect); apiRouter.post("/modbus/connect", modbusController.connect);
apiRouter.post("/modbus/close", modbusController.close); apiRouter.post("/modbus/close", modbusController.close);
apiRouter.get("/modbus/data_stream", modbusController.streamData); apiRouter.get("/modbus/data_stream", modbusController.streamData);

View File

@ -1,11 +1,11 @@
import db from '#root/config/database.config.js' import db from "#root/config/database.config.js";
async function run() { async function run() {
try { try {
await db.migrate({ force: false }) await db.migrate({ force: false });
console.log('Database initialized.'); console.log("Database initialized.");
} catch (error) { } catch (error) {
throw error; console.error(error);
} }
} }