mirror of
https://github.com/sergeyyarkov/modbus-logger.git
synced 2026-10-11 10:28:51 +03:00
progress
This commit is contained in:
parent
1cdf164fba
commit
8932402d3b
@ -85,7 +85,16 @@ export const modbusController = {
|
|||||||
* @param {import('express').Response} res
|
* @param {import('express').Response} res
|
||||||
* @param {import('express').NextFunction} next
|
* @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
|
* Create display value for modbus slave device
|
||||||
@ -112,7 +121,27 @@ export const modbusController = {
|
|||||||
async getLatestLog(req, res, next) {},
|
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').Request} req
|
||||||
* @param {import('express').Response} res
|
* @param {import('express').Response} res
|
||||||
* @param {import('express').NextFunction} next
|
* @param {import('express').NextFunction} next
|
||||||
|
|||||||
5
index.d.ts
vendored
5
index.d.ts
vendored
@ -10,9 +10,10 @@ export type AppConfig = {
|
|||||||
log_interval_ms: number;
|
log_interval_ms: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ModbusSlave = {
|
export type SlaveModbusDevice = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
display_addr: number;
|
display_reg_addr: number | null;
|
||||||
|
display_reg_format: number | null;
|
||||||
is_logging: boolean;
|
is_logging: boolean;
|
||||||
}
|
}
|
||||||
@ -21,8 +21,11 @@ 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_reg_addr" INTEGER CHECK(display_reg_addr >= 0 AND display_reg_addr <= 65534),
|
-- Holding register
|
||||||
"display_reg_format" INTEGER CHECK(display_reg_format IN(16, 32)),
|
"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,
|
"is_logging" BOOLEAN NOT NULL,
|
||||||
PRIMARY KEY("id")
|
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
|
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
|
-- Down
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|||||||
@ -603,19 +603,27 @@ video {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.z-50 {
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
.mx-auto {
|
.mx-auto {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.my-2 {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.my-4 {
|
.my-4 {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.my-2 {
|
.mb-3 {
|
||||||
margin-top: 0.5rem;
|
margin-bottom: 0.75rem;
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ml-auto {
|
.ml-auto {
|
||||||
@ -630,6 +638,22 @@ video {
|
|||||||
margin-right: 0.5rem;
|
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 {
|
.mt-16 {
|
||||||
margin-top: 4rem;
|
margin-top: 4rem;
|
||||||
}
|
}
|
||||||
@ -638,6 +662,10 @@ video {
|
|||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mt-20 {
|
||||||
|
margin-top: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.mt-3 {
|
.mt-3 {
|
||||||
margin-top: 0.75rem;
|
margin-top: 0.75rem;
|
||||||
}
|
}
|
||||||
@ -646,8 +674,12 @@ video {
|
|||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mt-10 {
|
.mt-5 {
|
||||||
margin-top: 2.5rem;
|
margin-top: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-6 {
|
||||||
|
margin-top: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.block {
|
.block {
|
||||||
@ -706,14 +738,14 @@ video {
|
|||||||
max-width: 42rem;
|
max-width: 42rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.max-w-6xl {
|
|
||||||
max-width: 72rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.max-w-5xl {
|
.max-w-5xl {
|
||||||
max-width: 64rem;
|
max-width: 64rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.max-w-md {
|
||||||
|
max-width: 28rem;
|
||||||
|
}
|
||||||
|
|
||||||
.flex-1 {
|
.flex-1 {
|
||||||
flex: 1 1 0%;
|
flex: 1 1 0%;
|
||||||
}
|
}
|
||||||
@ -762,6 +794,10 @@ video {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.justify-end {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.justify-between {
|
.justify-between {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
@ -772,6 +808,12 @@ 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-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]) {
|
.space-y-2 > :not([hidden]) ~ :not([hidden]) {
|
||||||
--tw-space-y-reverse: 0;
|
--tw-space-y-reverse: 0;
|
||||||
margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
|
margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
|
||||||
@ -816,6 +858,21 @@ video {
|
|||||||
border-style: dashed;
|
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-blue-600 {
|
||||||
fill: #2563eb;
|
fill: #2563eb;
|
||||||
}
|
}
|
||||||
@ -862,15 +919,37 @@ video {
|
|||||||
padding-right: 0.625rem;
|
padding-right: 0.625rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.px-4 {
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.py-3 {
|
.py-3 {
|
||||||
padding-top: 0.75rem;
|
padding-top: 0.75rem;
|
||||||
padding-bottom: 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 {
|
.pt-5 {
|
||||||
padding-top: 1.25rem;
|
padding-top: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
.text-center {
|
.text-center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
@ -885,9 +964,14 @@ video {
|
|||||||
line-height: 2.25rem;
|
line-height: 2.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-5xl {
|
.text-4xl {
|
||||||
font-size: 3rem;
|
font-size: 2.25rem;
|
||||||
line-height: 1;
|
line-height: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-lg {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
line-height: 1.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-sm {
|
.text-sm {
|
||||||
@ -895,6 +979,11 @@ video {
|
|||||||
line-height: 1.25rem;
|
line-height: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-xl {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.font-bold {
|
.font-bold {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
@ -913,16 +1002,40 @@ video {
|
|||||||
color: rgb(156 163 175 / var(--tw-text-opacity));
|
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 {
|
.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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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 {
|
.text-red-600 {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(220 38 38 / var(--tw-text-opacity));
|
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-70 {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
@ -943,6 +1056,24 @@ video {
|
|||||||
display: none !important;
|
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,
|
input,
|
||||||
select {
|
select {
|
||||||
margin-top: 0.125rem;
|
margin-top: 0.125rem;
|
||||||
@ -971,6 +1102,11 @@ button:disabled {
|
|||||||
pointer-events: all !important;
|
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 {
|
.hover\:bg-gray-50:hover {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
|
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
|
||||||
|
|||||||
@ -12,6 +12,14 @@
|
|||||||
display: none !important;
|
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,
|
input,
|
||||||
select {
|
select {
|
||||||
@apply w-full border py-1 rounded-sm my-0.5 bg-white;
|
@apply w-full border py-1 rounded-sm my-0.5 bg-white;
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
<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" />
|
||||||
|
<link rel="stylesheet" href="/css/dygraphs.css" />
|
||||||
<script defer src="/js/dygraphs.min.js"></script>
|
<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>
|
||||||
@ -17,48 +18,69 @@
|
|||||||
<!-- Root element -->
|
<!-- Root element -->
|
||||||
<div x-data x-cloak class="mx-auto max-w-5xl flex flex-col h-screen">
|
<div x-data x-cloak class="mx-auto max-w-5xl flex flex-col h-screen">
|
||||||
<!-- Monitoring content -->
|
<!-- Monitoring content -->
|
||||||
<main class="flex-1" x-bind:hidden="$store.app.currentPage !== 'monitoring'">
|
<template x-if="$store.app.currentPage === 'monitoring'">
|
||||||
|
<main x-data="monitoringPage" 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 h-full flex-row space-x-2">
|
<div class="flex h-full flex-row space-x-2">
|
||||||
<div class="border-r min-w-[256px] p-2.5 pt-5">
|
<div class="border-r min-w-[256px] p-2.5 pt-5">
|
||||||
<div class="flex flex-col space-y-2">
|
<div class="flex flex-col space-y-1">
|
||||||
<span class="italic text-sm">Devices list:</span>
|
<span class="italic text-sm">Devices:</span>
|
||||||
|
<span x-show="devices.length === 0" class="text-gray-500 text-sm text-center block">No devices added.</span>
|
||||||
<div class="flex flex-col max-h-[520px] overflow-y-auto space-y-2">
|
<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">
|
<template x-for="device in devices">
|
||||||
<h2 class="text-md font-bold">Device #1</h2>
|
<button @click="selectDevice(device)" :class="{'bg-gray-100': selectedDevice?.id === device.id}" class="w-full select-none border px-2.5 py-3 text-left rounded-sm hover:bg-gray-100 cursor-pointer">
|
||||||
</div>
|
<span x-text="device.name" class="text-md font-bold"></span>
|
||||||
<div class="w-full select-none border px-2.5 py-3 rounded-sm hover:bg-gray-50 cursor-pointer">
|
</button>
|
||||||
<h2 class="text-md font-bold">Device #2</h2>
|
</template>
|
||||||
</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>
|
||||||
|
<button class="w-full text-center border-2 border-dashed rounded-sm hover:bg-gray-50 cursor-pointer text-left">
|
||||||
|
<span class="text-4xl text-gray-400 select-none">+</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<hr class="my-4" />
|
<hr class="my-4" />
|
||||||
<div><span class="italic text-sm">Connection:</span><br /></div>
|
<div>
|
||||||
|
<span class="italic text-sm">Connection:</span>
|
||||||
|
<div class="flex flex-col mt-1.5">
|
||||||
|
<span class="text-sm">Status:
|
||||||
|
<b x-show="$store.app.is_opened" class="text-green-700">Opened</b>
|
||||||
|
<b x-show="!$store.app.is_opened" class="text-red-700">Closed</b>
|
||||||
|
</span>
|
||||||
|
<a class="text-sm" @click="$store.app.setPage('configuration')" href="#">
|
||||||
|
Reconfigurate
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-auto pl-2">
|
||||||
|
<div x-show="selectedDevice != null">
|
||||||
|
<div class="pt-5 border-b flex justify-between items-center pr-6">
|
||||||
|
<h2 x-text="selectedDevice?.name" class="font-bold text-3xl my-2"></h2>
|
||||||
|
<div>
|
||||||
|
<a href="#">Edit</a>
|
||||||
|
<a @click="$store.confirmDialog.open('Remove device', `Delete ${selectedDevice.name}? All logs will be cleared.`, removeDevice, selectedDevice.id)" class="text-red-500" href="#">Delete</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-auto">
|
|
||||||
<div class="pt-5 border-b">
|
|
||||||
<h2 class="font-bold text-3xl my-2">Device #1</h2>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<div id="div_g" style="width: 720px; height: 300px"></div>
|
<div id="div_g" x-ref="graph" style="width: 720px; height: 300px"></div>
|
||||||
</div>
|
</div>
|
||||||
<hr class="mt-10" />
|
<hr class="mt-10" />
|
||||||
|
<h2 class="text-xl font-bold mb-3">Display values</h2>
|
||||||
|
</div>
|
||||||
|
<div class="select-none" x-show="selectedDevice === null">
|
||||||
|
<span class="block text-center italic text-gray-500 mt-10">No device selected.</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="border-l"></div> -->
|
<!-- <div class="border-l"></div> -->
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Configuration content -->
|
<!-- Configuration content -->
|
||||||
<main x-data="configAppPage" x-bind:hidden="$store.app.currentPage !== 'configuration'">
|
<template x-if="$store.app.currentPage === 'configuration'">
|
||||||
|
<main class="mt-6" x-data="configAppPage" x-bind:hidden="$store.app.currentPage !== 'configuration'">
|
||||||
<div class="border rounded-sm p-4 max-w-2xl mx-auto">
|
<div class="border rounded-sm p-4 max-w-2xl mx-auto">
|
||||||
<h2 class="text-2xl font-bold">Setup configuration</h2>
|
<h2 class="text-2xl font-bold">Setup configuration</h2>
|
||||||
<p class="text-gray-700 italic">
|
<p class="text-gray-700 italic">
|
||||||
@ -210,8 +232,10 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Loading content -->
|
<!-- Loading content -->
|
||||||
|
<template x-if="$store.app.currentPage === 'loading'">
|
||||||
<main x-bind:hidden="$store.app.currentPage !== 'loading'">
|
<main x-bind:hidden="$store.app.currentPage !== 'loading'">
|
||||||
<div class="text-center mt-16">
|
<div class="text-center mt-16">
|
||||||
<div role="status">
|
<div role="status">
|
||||||
@ -235,8 +259,10 @@
|
|||||||
<p class="mt-4">Loading...</p>
|
<p class="mt-4">Loading...</p>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Error content -->
|
<!-- Error content -->
|
||||||
|
<template x-if="$store.app.currentPage === 'error'">
|
||||||
<main x-bind:hidden="$store.app.currentPage !== 'error'">
|
<main x-bind:hidden="$store.app.currentPage !== 'error'">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-bold">Something went wrong.</h1>
|
<h1 class="text-2xl font-bold">Something went wrong.</h1>
|
||||||
@ -247,13 +273,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- 404 content -->
|
<!-- 404 content -->
|
||||||
|
<template x-if="$store.app.currentPage === '404'">
|
||||||
<main x-bind:hidden="$store.app.currentPage !== '404'">
|
<main x-bind:hidden="$store.app.currentPage !== '404'">
|
||||||
<div>
|
<div class="text-center mt-5">
|
||||||
<h1 class="text 2xl font-bold">Page not found</h1>
|
<h1 class="text-2xl font-bold">Page not found</h1>
|
||||||
|
<a @click="$store.app.setPage('monitoring')" class="underline text-blue" href="#">return to monitoring</a>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div x-show="$store.confirmDialog.isOpen" class="backdrop z-50">
|
||||||
|
<div role="dialog" @click.away="$store.confirmDialog.close()" class="px-4 py-3.5 ml-auto mr-auto mt-20 bg-white max-w-md border rounded-sm">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<h2 x-text="$store.confirmDialog.labels.title" class="font-bold text-lg"></h2>
|
||||||
|
<button @click="$store.confirmDialog.close()" aria-label="Close">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M6.4 19L5 17.6l5.6-5.6L5 6.4L6.4 5l5.6 5.6L17.6 5L19 6.4L13.4 12l5.6 5.6l-1.4 1.4l-5.6-5.6L6.4 19Z"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<p x-text="$store.confirmDialog.labels.body"></p>
|
||||||
|
<div class="mt-4 flex justify-end space-x-2">
|
||||||
|
<button @click="$store.confirmDialog.confirm()" class="btn">Confirm</button>
|
||||||
|
<button @click="$store.confirmDialog.close()" class="btn bg-red-500">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -4,34 +4,18 @@ 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"];
|
||||||
|
let graphData = [];
|
||||||
var data = [];
|
const t = new Date();
|
||||||
var t = new Date();
|
|
||||||
|
|
||||||
for (var i = 10; i >= 0; i--) {
|
for (var i = 10; i >= 0; i--) {
|
||||||
var x = new Date(t.getTime() - i * 1000);
|
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 () => {
|
document.addEventListener("alpine:init", async () => {
|
||||||
Alpine.store("app", {
|
Alpine.store("app", {
|
||||||
is_configured: false,
|
is_configured: false,
|
||||||
|
is_opened: false,
|
||||||
config: {
|
config: {
|
||||||
mb_connection_type: "TCP",
|
mb_connection_type: "TCP",
|
||||||
mb_tcp_ip: "127.0.0.1",
|
mb_tcp_ip: "127.0.0.1",
|
||||||
@ -45,9 +29,9 @@ document.addEventListener("alpine:init", async () => {
|
|||||||
},
|
},
|
||||||
currentPage: "loading",
|
currentPage: "loading",
|
||||||
error: null,
|
error: null,
|
||||||
devices: [],
|
|
||||||
async init() {
|
async init() {
|
||||||
try {
|
try {
|
||||||
|
const connStatusEventSource = new EventSource('/api/modbus/status');
|
||||||
const { config } = await api.get("/app/config");
|
const { config } = await api.get("/app/config");
|
||||||
const { is_configured } = await api.get("/app/is_configured");
|
const { is_configured } = await api.get("/app/is_configured");
|
||||||
|
|
||||||
@ -55,14 +39,15 @@ 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");
|
connStatusEventSource.onmessage = (e) => (this.is_opened = e.data === 'true');
|
||||||
this.devices = devices;
|
await utils.wait(500);
|
||||||
this.currentPage = "monitoring";
|
this.currentPage = "monitoring";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.currentPage = "configuration";
|
this.currentPage = "configuration";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
this.currentPage = "error";
|
this.currentPage = "error";
|
||||||
this.error = 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", () => ({
|
Alpine.data("configAppPage", () => ({
|
||||||
selectedConnectionType: "TCP",
|
selectedConnectionType: "TCP",
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@ -99,6 +124,7 @@ document.addEventListener("alpine:init", async () => {
|
|||||||
};
|
};
|
||||||
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");
|
||||||
|
await utils.wait(500);
|
||||||
this.$store.app.currentPage = "monitoring";
|
this.$store.app.currentPage = "monitoring";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.errorMessage = `${error.message}`;
|
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;
|
||||||
|
}
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1 +1,3 @@
|
|||||||
export * from './object.js'
|
export * from './object.js'
|
||||||
|
|
||||||
|
export const wait = (ms) => new Promise((res) => setTimeout(() => res(), ms));
|
||||||
@ -3,7 +3,7 @@ import express from "express";
|
|||||||
/**
|
/**
|
||||||
* Validators
|
* Validators
|
||||||
*/
|
*/
|
||||||
import { appConfigSchema } from "#root/validators/index.js";
|
import { appConfigSchema, removeModbusDeviceSchema } from "#root/validators/index.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controllers
|
* Controllers
|
||||||
@ -22,8 +22,10 @@ apiRouter.patch("/app/config", validateBodyMiddleware(appConfigSchema), appContr
|
|||||||
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/remove_device', validateBodyMiddleware(removeModbusDeviceSchema), modbusController.removeSlaveDevice);
|
||||||
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);
|
||||||
|
apiRouter.get('/modbus/status', modbusController.status);
|
||||||
|
|
||||||
export { apiRouter };
|
export { apiRouter };
|
||||||
|
|||||||
@ -2,7 +2,7 @@ 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: true });
|
||||||
console.log("Database initialized.");
|
console.log("Database initialized.");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|||||||
@ -16,3 +16,5 @@ export const appConfigSchema = object({
|
|||||||
mb_rtu_stop_bits: number().oneOf([1, 2]).when("mb_connection_type", requiredIfConnTypeEquals("RTU")),
|
mb_rtu_stop_bits: number().oneOf([1, 2]).when("mb_connection_type", requiredIfConnTypeEquals("RTU")),
|
||||||
log_interval_ms: number().min(1000).required(),
|
log_interval_ms: number().min(1000).required(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const removeModbusDeviceSchema = object({ id: number().max(255).min(1).required() })
|
||||||
Loading…
x
Reference in New Issue
Block a user