mirror of
https://github.com/sergeyyarkov/modbus-logger.git
synced 2026-10-11 02:18:51 +03:00
progress
This commit is contained in:
parent
8a0cfe5063
commit
90271dc45f
@ -65,7 +65,23 @@ export const modbusController = {
|
||||
*/
|
||||
async list(req, res, next) {
|
||||
try {
|
||||
const slaves = await db.all('SELECT * from modbus_slaves');
|
||||
const slaves = await db.all(`
|
||||
SELECT ms.id AS id, ms.name AS name, g_display_reg_addr, g_display_reg_format, g_y_label, is_logging,
|
||||
CASE
|
||||
WHEN COUNT(dv.id) = 0 THEN '[]'
|
||||
ELSE '[' || GROUP_CONCAT(
|
||||
JSON_OBJECT(
|
||||
'id', dv.id,
|
||||
'name', dv.name,
|
||||
'reg_addr', dv.reg_addr,
|
||||
'reg_format', dv.reg_format
|
||||
), ', '
|
||||
) || ']' END AS display_values
|
||||
FROM modbus_slaves AS ms
|
||||
LEFT JOIN display_values AS dv ON dv.slave_id = ms.id
|
||||
GROUP BY ms.id, ms.name;
|
||||
`);
|
||||
slaves.forEach(s => (s.display_values = JSON.parse(s.display_values)))
|
||||
return res.status(200).json(slaves);
|
||||
} catch (error) {
|
||||
next(error)
|
||||
@ -80,6 +96,7 @@ export const modbusController = {
|
||||
*/
|
||||
async createDevice(req, res, next) {
|
||||
try {
|
||||
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" (
|
||||
"id",
|
||||
"name",
|
||||
@ -88,8 +105,8 @@ export const modbusController = {
|
||||
"g_y_label",
|
||||
"is_logging")
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[...req.body]);
|
||||
return res.send(200).json({ message: 'Device created.', data: req.body });
|
||||
[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) {
|
||||
next(error)
|
||||
}
|
||||
@ -140,7 +157,14 @@ export const modbusController = {
|
||||
* @param {import('express').Response} res
|
||||
* @param {import('express').NextFunction} next
|
||||
*/
|
||||
async removeDisplayValue(req, res, next) {},
|
||||
async removeDisplayValue(req, res, next) {
|
||||
try {
|
||||
await db.run(`DELETE FROM "display_values" WHERE id = ?`, req.body.id);
|
||||
return res.status(200).json({ message: 'Display value removed.' });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get latest data log from modbus slave device
|
||||
|
||||
19
getslaves.sql
Normal file
19
getslaves.sql
Normal file
@ -0,0 +1,19 @@
|
||||
SELECT
|
||||
ms.id AS id,
|
||||
ms.name AS name,
|
||||
g_display_reg_addr,
|
||||
g_display_reg_format,
|
||||
g_y_label,
|
||||
is_logging,
|
||||
CASE
|
||||
WHEN COUNT(dv.id) = 0 THEN '[]'
|
||||
ELSE '[' || GROUP_CONCAT(
|
||||
JSON_OBJECT(
|
||||
'name', dv.name,
|
||||
'reg_addr', dv.reg_addr,
|
||||
'reg_format', dv.reg_format
|
||||
), ', '
|
||||
) || ']' END AS display_values
|
||||
FROM modbus_slaves AS ms
|
||||
LEFT JOIN display_values AS dv ON dv.slave_id = ms.id
|
||||
GROUP BY ms.id, ms.name;
|
||||
@ -22,19 +22,20 @@ 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),
|
||||
"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)),
|
||||
"g_y_label" VARCHAR(40),
|
||||
"is_logging" BOOLEAN NOT NULL,
|
||||
"g_display_reg_format" INTEGER CHECK(g_display_reg_format IN(16, 32)) DEFAULT 16,
|
||||
"g_y_label" VARCHAR(40) DEFAULT "Label",
|
||||
"is_logging" BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
PRIMARY KEY("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "display_values" (
|
||||
"id" INTEGER,
|
||||
"name" VARCHAR(32),
|
||||
"name" VARCHAR(32) NOT NULL,
|
||||
"slave_id" INTEGER NOT NULL,
|
||||
"reg_addr" INTEGER DEFAULT 0 NOT NULL CHECK(reg_addr >= 0 AND reg_addr <= 65534),
|
||||
"reg_format" INTEGER NOT NULL CHECK(reg_format IN(16, 32)) DEFAULT 16,
|
||||
PRIMARY KEY("id"),
|
||||
FOREIGN KEY("slave_id") REFERENCES modbus_slaves("id") ON DELETE CASCADE
|
||||
);
|
||||
@ -62,6 +63,17 @@ INSERT INTO "modbus_slaves" (
|
||||
(3, "Device #3", 8, 16, "Label 3", 0),
|
||||
(4, "Device #4", NULL, NULL, "Label 4", 0);
|
||||
|
||||
INSERT INTO "display_values" (
|
||||
"id", "name", "slave_id", "reg_addr", "reg_format"
|
||||
) VALUES
|
||||
(1, "CV1", 1, 0, 16),
|
||||
(2, "CV2", 1, 1, 16),
|
||||
(3, "SP", 1, 2, 16),
|
||||
(4, "CV1", 2, 3, 16),
|
||||
(5, "CV2", 2, 4, 16),
|
||||
(6, "SP", 2, 5, 16),
|
||||
(7, "CV1", 3, 6, 16),
|
||||
(8, "SP", 3, 7, 16);
|
||||
--------------------------------------------------------------------------------
|
||||
-- Down
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
@ -632,16 +632,16 @@ video {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.my-4 {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.my-3 {
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.my-4 {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
@ -662,10 +662,6 @@ video {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.mr-auto {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.mt-1 {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
@ -686,10 +682,6 @@ video {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.mt-20 {
|
||||
margin-top: 5rem;
|
||||
}
|
||||
|
||||
.mt-3 {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
@ -706,10 +698,6 @@ video {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.mt-auto {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
@ -750,6 +738,10 @@ video {
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.min-h-\[140px\] {
|
||||
min-height: 140px;
|
||||
}
|
||||
|
||||
.min-h-\[50px\] {
|
||||
min-height: 50px;
|
||||
}
|
||||
@ -766,31 +758,10 @@ video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.w-16 {
|
||||
width: 4rem;
|
||||
}
|
||||
|
||||
.w-24 {
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
.w-52 {
|
||||
width: 13rem;
|
||||
}
|
||||
|
||||
.min-w-\[256px\] {
|
||||
min-width: 256px;
|
||||
}
|
||||
|
||||
.min-w-fit {
|
||||
min-width: -moz-fit-content;
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
.min-w-\[100px\] {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.max-w-2xl {
|
||||
max-width: 42rem;
|
||||
}
|
||||
@ -799,10 +770,6 @@ video {
|
||||
max-width: 64rem;
|
||||
}
|
||||
|
||||
.max-w-md {
|
||||
max-width: 28rem;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
@ -811,10 +778,6 @@ video {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.basis-52 {
|
||||
flex-basis: 13rem;
|
||||
}
|
||||
|
||||
.transform {
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
@ -843,42 +806,10 @@ video {
|
||||
resize: both;
|
||||
}
|
||||
|
||||
.grid-flow-col {
|
||||
grid-auto-flow: column;
|
||||
}
|
||||
|
||||
.grid-cols-4 {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid-cols-3 {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid-cols-\[200px_minmax\(900px\2c _1fr\)_100px\] {
|
||||
grid-template-columns: 200px minmax(900px, 1fr) 100px;
|
||||
}
|
||||
|
||||
.grid-cols-1 {
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid-rows-3 {
|
||||
grid-template-rows: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid-rows-4 {
|
||||
grid-template-rows: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid-rows-1 {
|
||||
grid-template-rows: repeat(1, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid-rows-2 {
|
||||
grid-template-rows: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
@ -891,6 +822,10 @@ video {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.items-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
@ -907,10 +842,6 @@ video {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.gap-3 {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
@ -939,10 +870,6 @@ video {
|
||||
margin-bottom: calc(0.75rem * var(--tw-space-y-reverse));
|
||||
}
|
||||
|
||||
.overflow-auto {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.overflow-y-auto {
|
||||
overflow-y: auto;
|
||||
}
|
||||
@ -951,10 +878,6 @@ video {
|
||||
border-radius: 0.125rem;
|
||||
}
|
||||
|
||||
.rounded-md {
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.border {
|
||||
border-width: 1px;
|
||||
}
|
||||
@ -993,11 +916,6 @@ video {
|
||||
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;
|
||||
}
|
||||
@ -1044,19 +962,13 @@ 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-0 {
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
.pl-2 {
|
||||
@ -1071,10 +983,6 @@ video {
|
||||
padding-top: 1.25rem;
|
||||
}
|
||||
|
||||
.pl-0 {
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
@ -1098,6 +1006,11 @@ video {
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
|
||||
.text-5xl {
|
||||
font-size: 3rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.text-lg {
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.75rem;
|
||||
@ -1113,11 +1026,6 @@ video {
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
.text-5xl {
|
||||
font-size: 3rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
@ -1221,6 +1129,12 @@ select {
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
margin: 0px;
|
||||
width: auto;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
input {
|
||||
padding-left: 0.375rem;
|
||||
padding-right: 0.375rem;
|
||||
@ -1253,22 +1167,12 @@ button:disabled {
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.sm\:grid-cols-1 {
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@media (min-width: 640px) {
|
||||
.md\:sm\:grid-cols-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.md\:grid-cols-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
@ -1277,8 +1181,4 @@ button:disabled {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.lg\:grid-cols-3 {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,6 +29,10 @@ select {
|
||||
@apply w-full border py-1 rounded-sm my-0.5 bg-white;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
@apply w-auto m-0 align-middle;
|
||||
}
|
||||
|
||||
input {
|
||||
@apply px-1.5;
|
||||
}
|
||||
|
||||
@ -20,10 +20,6 @@
|
||||
<!-- Monitoring content -->
|
||||
<template x-if="$store.app.currentPage === 'monitoring'">
|
||||
<main x-data="monitoringPage" class="flex-1" x-bind:hidden="$store.app.currentPage !== 'monitoring'">
|
||||
<!-- <div>
|
||||
<h1 class="text-3xl font-bold">Monitoring page</h1>
|
||||
<button @click="$store.app.setPage('configuration')" class="btn">Config</button>
|
||||
</div> -->
|
||||
<div class="flex h-full flex-row space-x-2">
|
||||
<div class="border-r min-w-[256px] p-2.5 pl-0 pt-5">
|
||||
<div class="flex flex-col space-y-1">
|
||||
@ -32,13 +28,12 @@
|
||||
<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-[400px] overflow-y-auto space-y-2">
|
||||
<div @add-device.window="devices = [...devices, $event.detail]; selectedDevice = $event.detail" class="flex flex-col max-h-[400px] overflow-y-auto space-y-2">
|
||||
<template x-for="device in devices" :key="device.id">
|
||||
<button
|
||||
@click="selectDevice(device)"
|
||||
:class="{'bg-gray-100': selectedDevice?.id === device.id}"
|
||||
class="w-full min-h-[50px] select-none border px-2.5 py-3 text-left rounded-sm hover:bg-gray-100 cursor-pointer"
|
||||
>
|
||||
class="w-full min-h-[50px] select-none border px-2.5 py-3 text-left rounded-sm hover:bg-gray-100 cursor-pointer">
|
||||
<span x-text="device.name" class="text-md font-bold"></span>
|
||||
</button>
|
||||
</template>
|
||||
@ -46,14 +41,15 @@
|
||||
<!-- MODAL: Create new device -->
|
||||
<template x-teleport="body">
|
||||
<div
|
||||
@keyup.escape.window="modalCreateDevice.close()"
|
||||
x-data="deviceModal"
|
||||
@open-create-device-modal.window="open()"
|
||||
@keyup.escape.window="close()"
|
||||
class="backdrop z-50"
|
||||
x-show="modalCreateDevice.isOpen"
|
||||
>
|
||||
x-show="isOpen">
|
||||
<div class="modal">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="font-bold text-lg">Create new device</h2>
|
||||
<button @click="modalCreateDevice.close()" aria-label="Close">
|
||||
<button @click="close()" aria-label="Close">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
@ -63,30 +59,60 @@
|
||||
</button>
|
||||
</div>
|
||||
<hr />
|
||||
<form x-on:submit.prevent="createDevice" action="#">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<form x-data="{ graphView: '' }" @submit.prevent="create(() => graphView = 'none')" action="#">
|
||||
<div x-id="['id', 'name', 'graph-display', 'g_display_reg_addr', 'g_display_reg_format', 'g_y_label', 'is_logging']" class="flex flex-col space-y-2">
|
||||
<div>
|
||||
<label for="id">Slave Id</label>
|
||||
<input placeholder="1" id="id" name="id" type="number" min="1" max="255" required />
|
||||
<label :for="$id('id')"><span class="text-red-500">*</span>Slave ID:</label>
|
||||
<input x-model.number="data.id" :id="$id('id')" name="id" placeholder="1" type="number" min="1" max="255" required />
|
||||
</div>
|
||||
<div>
|
||||
<label for="id">Name</label>
|
||||
<input placeholder="Device #1" id="id" name="id" type="text" required />
|
||||
<label :for="$id('name')"><span class="text-red-500">*</span>Name:</label>
|
||||
<input x-model="data.name" placeholder="Device #1" :id="$id('name')" name="name" type="text" required />
|
||||
</div>
|
||||
<div>
|
||||
<label :for="$id('graph-display')">Graph Display</label>
|
||||
<select x-model="graphView" :id="$id('graph-display')">
|
||||
<option value="none">None</option>
|
||||
<option value="new-value">New value</option>
|
||||
</select>
|
||||
</div>
|
||||
<div x-show="graphView === 'new-value'">
|
||||
<div>
|
||||
<label :for="$id('g_y_label')"><span class="text-red-500">*</span>Value name:</label>
|
||||
<input x-model="data.g_y_label" :id="$id('g_y_label')" :required="graphView === 'new-value'" name="g_y_label" placeholder="PV" type="text" />
|
||||
</div>
|
||||
<div>
|
||||
<label :for="$id('g_display_reg_addr')"><span class="text-red-500">*</span>Register address:</label>
|
||||
<input x-model="data.g_display_reg_addr" :id="$id('g_display_reg_addr')" name="g_display_reg_addr" placeholder="1" type="number" min="0" max="65534" :required="graphView === 'new-value'" />
|
||||
</div>
|
||||
<div>
|
||||
<label :for="$id('g_display_reg_format')"><span class="text-red-500">*</span>Format (bits):</label>
|
||||
<select x-model.number="data.g_display_reg_format" :id="$id('g_display_reg_format')" name="g_display_reg_format">
|
||||
<option value="16">16</option>
|
||||
<option value="32">32</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<input x-model="data.is_logging" :id="$id('is_logging')" name="is_logging" type="checkbox" />
|
||||
<label :for="$id('is_logging')">Logging</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span x-text="error?.message" class="text-red-500"></span>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end space-x-2">
|
||||
<button x-bind:disabled="isLoading" x-bind:class="isLoading && 'opacity-70'" type="submit" class="btn">Create</button>
|
||||
<button @click="close()" class="btn bg-red-500">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="mt-4 flex justify-end space-x-2">
|
||||
<button class="btn">Create</button>
|
||||
<button @click="modalCreateDevice.close()" class="btn bg-red-500">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Create new device -->
|
||||
<button
|
||||
@click="modalCreateDevice.open()"
|
||||
class="w-full min-h-[50px] text-center border-2 border-dashed rounded-sm hover:bg-gray-50 cursor-pointer"
|
||||
>
|
||||
@click="$dispatch('open-create-device-modal')"
|
||||
class="w-full min-h-[50px] text-center border-2 border-dashed rounded-sm hover:bg-gray-50 cursor-pointer">
|
||||
<span class="text-4xl text-gray-400 select-none">+</span>
|
||||
</button>
|
||||
</div>
|
||||
@ -107,25 +133,25 @@
|
||||
<div class="flex-auto pl-2">
|
||||
<div x-show="selectedDevice != null">
|
||||
<div class="pt-5 border-b flex flex-wrap justify-between items-center pr-6">
|
||||
<h2 x-text="selectedDevice?.name" class="font-bold text-3xl my-2 mr-2"></h2>
|
||||
<!-- Device create, delete -->
|
||||
<div class="mb-2">
|
||||
<h2 x-text="selectedDevice?.name" class="font-bold text-3xl my-2 mr-2"></h2>
|
||||
<span>ID: </span><span x-text="selectedDevice?.id"></span>
|
||||
</div>
|
||||
<!-- Device create, delete buttons -->
|
||||
<div>
|
||||
<a href="#">Edit</a>
|
||||
<a
|
||||
@click="$dispatch('dialog', {
|
||||
<a @click="$dispatch('dialog', {
|
||||
title: 'Remove device',
|
||||
body: `Remove ${selectedDevice.name}? All logs will be cleared.`,
|
||||
isOpen: true,
|
||||
onConfirm: async () => await removeDevice(selectedDevice.id)
|
||||
});"
|
||||
class="text-red-500"
|
||||
href="#"
|
||||
>
|
||||
href="#">
|
||||
Delete
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Display Graph -->
|
||||
<div x-show="selectedDevice?.g_display_reg_addr != null" class="mt-4">
|
||||
<div id="div_g" style="width: 720px; height: 300px"></div>
|
||||
@ -133,13 +159,14 @@
|
||||
</div>
|
||||
<!-- Display values -->
|
||||
<h2 class="text-xl font-bold mb-3 mt-5">Display values</h2>
|
||||
<div>
|
||||
<div class="mb-2">
|
||||
<div class="grid grid-cols-1 md:sm:grid-cols-2 lg:sm:grid-cols-3 gap-2">
|
||||
<template x-for="value in [1,2,3,4,5]">
|
||||
<template x-for="value in selectedDevice?.display_values" :key="value.id">
|
||||
<div class="border-2 p-2.5 rounded-sm">
|
||||
<div class="flex justify-between">
|
||||
<h3 class="text-xl font-bold">CV1</h3>
|
||||
<div>
|
||||
<div class="flex justify-between items-start">
|
||||
<h3 x-text="value.name" class="text-xl font-bold"></h3>
|
||||
<div class="flex">
|
||||
<!-- Edit display value -->
|
||||
<button>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path
|
||||
@ -148,7 +175,13 @@
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button>
|
||||
<!-- Remove display value -->
|
||||
<button @click="$dispatch('dialog', {
|
||||
title: 'Remove display value',
|
||||
body: `Remove display value ${value.name}?`,
|
||||
isOpen: true,
|
||||
onConfirm: async () => await removeDisplayValue(value.id)
|
||||
});">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="#ef4444"
|
||||
@ -160,14 +193,14 @@
|
||||
</div>
|
||||
<hr />
|
||||
<div class="flex flex-col">
|
||||
<span>Value: 7892</span>
|
||||
<span>Reg Addr: 6</span>
|
||||
<span>Format: 32</span>
|
||||
<span>Value: ---</span>
|
||||
<span>Reg Addr: <span x-text="value.reg_addr"></span></span>
|
||||
<span>Format: <span x-text="value.reg_format"></span></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
class="flex justify-center items-center border-4 text-center border-dashed p-2.5 rounded-sm cursor-pointer hover:bg-gray-50"
|
||||
class="flex justify-center min-h-[140px] items-center border-4 text-center border-dashed p-2.5 rounded-sm cursor-pointer hover:bg-gray-50"
|
||||
>
|
||||
<span class="text-5xl text-gray-400 font-bold select-none">+</span>
|
||||
</div>
|
||||
|
||||
@ -52,23 +52,58 @@ document.addEventListener("alpine:init", async () => {
|
||||
this.currentPage = "404";
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentPage = page;
|
||||
},
|
||||
});
|
||||
|
||||
Alpine.data('deviceModal', () => ({
|
||||
isOpen: false,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
data: {
|
||||
id: null,
|
||||
name: '',
|
||||
g_display_reg_addr: null,
|
||||
g_display_reg_format: 16,
|
||||
g_y_label: '',
|
||||
is_logging: false
|
||||
},
|
||||
resetDataFields() {
|
||||
this.data.id = null;
|
||||
this.data.name = '';
|
||||
this.data.g_display_reg_addr = null;
|
||||
this.data.g_display_reg_format = 16;
|
||||
this.data.g_y_label = '';
|
||||
this.data.is_logging = false;
|
||||
},
|
||||
async create(cb) {
|
||||
try {
|
||||
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();
|
||||
cb && cb();
|
||||
} catch (error) {
|
||||
this.error = error
|
||||
console.error(error);
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
open() {
|
||||
this.isOpen = true;
|
||||
},
|
||||
close() {
|
||||
this.isOpen = false;
|
||||
},
|
||||
}))
|
||||
|
||||
Alpine.data("monitoringPage", () => ({
|
||||
selectedDevice: null,
|
||||
devices: [],
|
||||
modalCreateDevice: {
|
||||
isOpen: false,
|
||||
open() {
|
||||
this.isOpen = true;
|
||||
},
|
||||
close() {
|
||||
this.isOpen = false;
|
||||
},
|
||||
},
|
||||
async init() {
|
||||
try {
|
||||
this.$store.app.currentPage = "loading";
|
||||
@ -92,7 +127,7 @@ document.addEventListener("alpine:init", async () => {
|
||||
},
|
||||
selectDevice(device) {
|
||||
if (device.id === this.selectedDevice?.id) return;
|
||||
graphData = [];
|
||||
graphData = []; // todo: reset graph
|
||||
this.selectedDevice = device;
|
||||
},
|
||||
async removeDevice(id) {
|
||||
@ -104,6 +139,14 @@ document.addEventListener("alpine:init", async () => {
|
||||
console.error("Error while removing device.", error);
|
||||
}
|
||||
},
|
||||
async removeDisplayValue(id) {
|
||||
try {
|
||||
await api.post('/modbus/remove_display-value', { id });
|
||||
this.selectedDevice.display_values = this.selectedDevice.display_values.filter(v => v.id != id);
|
||||
} catch (error) {
|
||||
console.error("Error while removing display value", error);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
Alpine.data("configAppPage", () => ({
|
||||
|
||||
@ -8,4 +8,10 @@ export function objKeysExclude(obj, keys) {
|
||||
const res = structuredClone(obj);
|
||||
Object.keys(res).forEach(k => keys.includes(k) && delete res[k]);
|
||||
return res;
|
||||
}
|
||||
|
||||
export function dellNullableKeys(obj) {
|
||||
const res = structuredClone(obj);
|
||||
Object.keys(res).forEach(k => obj[k] === null && delete res[k])
|
||||
return res;
|
||||
}
|
||||
18
routes/api/app.router.js
Normal file
18
routes/api/app.router.js
Normal file
@ -0,0 +1,18 @@
|
||||
import express from 'express'
|
||||
import cp from 'child_process'
|
||||
import { appConfigSchema } from "#root/validators/index.js";
|
||||
import { appController } from "#root/controllers/index.js";
|
||||
import { validateBodyMiddleware } from "#root/middlewares/index.js";
|
||||
|
||||
const appRouter = express.Router();
|
||||
|
||||
appRouter.get("/config", appController.getConfig);
|
||||
appRouter.patch("/config", validateBodyMiddleware(appConfigSchema), appController.updateConfig);
|
||||
appRouter.get("/is_configured", appController.isConfigured);
|
||||
appRouter.get('/ping', (req, res) => res.status(200).send('pong'));
|
||||
appRouter.get('/commit-hash', (req, res) => {
|
||||
const hash = cp.execSync('git rev-parse HEAD').toString().trim();
|
||||
return res.status(200).json({ hash })
|
||||
})
|
||||
|
||||
export { appRouter }
|
||||
@ -1,14 +1,11 @@
|
||||
import express from "express";
|
||||
import { modbusRouter } from './modbus.router.js'
|
||||
import { appConfigSchema } from "#root/validators/index.js";
|
||||
import { appController } from "#root/controllers/index.js";
|
||||
import { validateBodyMiddleware } from "#root/middlewares/index.js";
|
||||
import { appRouter } from "./app.router.js";
|
||||
|
||||
|
||||
const apiRouter = express.Router();
|
||||
|
||||
apiRouter.use("/modbus", modbusRouter);
|
||||
apiRouter.get("/app/config", appController.getConfig);
|
||||
apiRouter.patch("/app/config", validateBodyMiddleware(appConfigSchema), appController.updateConfig);
|
||||
apiRouter.get("/app/is_configured", appController.isConfigured);
|
||||
apiRouter.use('/app', appRouter)
|
||||
|
||||
export { apiRouter };
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import express from "express";
|
||||
import { modbusController } from "#root/controllers/index.js"
|
||||
import { modbusDeviceSchema, removeModbusDeviceSchema } from "#root/validators/index.js";
|
||||
import { modbusDeviceSchema, removeDisplayValueSchema, removeModbusDeviceSchema } from "#root/validators/index.js";
|
||||
import { validateBodyMiddleware } from "#root/middlewares/index.js";
|
||||
|
||||
const modbusRouter = express.Router();
|
||||
@ -9,6 +9,7 @@ modbusRouter.get("/devices", modbusController.list);
|
||||
modbusRouter.post("/create_device", validateBodyMiddleware(modbusDeviceSchema), modbusController.createDevice)
|
||||
modbusRouter.post('/remove_device', validateBodyMiddleware(removeModbusDeviceSchema), modbusController.removeDevice);
|
||||
modbusRouter.post('/update_device', validateBodyMiddleware(modbusDeviceSchema), modbusController.updateDevice);
|
||||
modbusRouter.post('/remove_display-value', validateBodyMiddleware(removeDisplayValueSchema), modbusController.removeDisplayValue);
|
||||
modbusRouter.post("/connect", modbusController.connect);
|
||||
modbusRouter.post("/close", modbusController.close);
|
||||
modbusRouter.get("/data_stream", modbusController.streamData);
|
||||
|
||||
11
validators/display-value.schema.js
Normal file
11
validators/display-value.schema.js
Normal file
@ -0,0 +1,11 @@
|
||||
import { object, number, boolean, string } from 'yup';
|
||||
|
||||
export const displayValueSchema = object({
|
||||
id: number().required(),
|
||||
name: string().required(),
|
||||
slave_id: number().required(),
|
||||
reg_addr: number().min(0).max(65534).required(),
|
||||
reg_format: number().oneOf([16, 32]).required()
|
||||
})
|
||||
|
||||
export const removeDisplayValueSchema = displayValueSchema.pick(['id']);
|
||||
@ -17,4 +17,5 @@ export const appConfigSchema = object({
|
||||
log_interval_ms: number().min(1000).required(),
|
||||
});
|
||||
|
||||
export * from './modbus-device.schema.js'
|
||||
export * from './modbus-device.schema.js'
|
||||
export * from './display-value.schema.js'
|
||||
@ -3,7 +3,7 @@ import { object, number, boolean, string } from 'yup';
|
||||
export const modbusDeviceSchema = object({
|
||||
id: number().required(),
|
||||
name: string().required(),
|
||||
g_display_reg_addr: number().nullable(),
|
||||
g_display_reg_addr: number().min(0).max(65534).nullable(),
|
||||
g_display_reg_format: number().oneOf([16, 32]).nullable(),
|
||||
g_y_label: string().max(40).nullable(),
|
||||
is_logging: boolean().required()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user