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
8a0cfe5063
commit
90271dc45f
@ -65,7 +65,23 @@ export const modbusController = {
|
|||||||
*/
|
*/
|
||||||
async list(req, res, next) {
|
async list(req, res, next) {
|
||||||
try {
|
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);
|
return res.status(200).json(slaves);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error)
|
next(error)
|
||||||
@ -80,6 +96,7 @@ export const modbusController = {
|
|||||||
*/
|
*/
|
||||||
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;
|
||||||
await db.run(`INSERT INTO "modbus_slaves" (
|
await db.run(`INSERT INTO "modbus_slaves" (
|
||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
@ -88,8 +105,8 @@ export const modbusController = {
|
|||||||
"g_y_label",
|
"g_y_label",
|
||||||
"is_logging")
|
"is_logging")
|
||||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||||
[...req.body]);
|
[id, name, g_display_reg_addr, g_display_reg_format, g_y_label, is_logging]);
|
||||||
return res.send(200).json({ message: 'Device created.', data: req.body });
|
return res.status(200).json({ message: 'Device created.' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error)
|
next(error)
|
||||||
}
|
}
|
||||||
@ -140,7 +157,14 @@ export const modbusController = {
|
|||||||
* @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 {
|
||||||
|
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
|
* 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),
|
"id" INTEGER CHECK (id >= 1 AND id <= 255),
|
||||||
"name" VARCHAR(256) NOT NULL,
|
"name" VARCHAR(256) NOT NULL,
|
||||||
-- Holding register
|
-- 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
|
-- Use 1 or 2 bytes
|
||||||
"g_display_reg_format" INTEGER CHECK(g_display_reg_format IN(16, 32)),
|
"g_display_reg_format" INTEGER CHECK(g_display_reg_format IN(16, 32)) DEFAULT 16,
|
||||||
"g_y_label" VARCHAR(40),
|
"g_y_label" VARCHAR(40) DEFAULT "Label",
|
||||||
"is_logging" BOOLEAN NOT NULL,
|
"is_logging" BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
PRIMARY KEY("id")
|
PRIMARY KEY("id")
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS "display_values" (
|
CREATE TABLE IF NOT EXISTS "display_values" (
|
||||||
"id" INTEGER,
|
"id" INTEGER,
|
||||||
"name" VARCHAR(32),
|
"name" VARCHAR(32) NOT NULL,
|
||||||
"slave_id" INTEGER NOT NULL,
|
"slave_id" INTEGER NOT NULL,
|
||||||
"reg_addr" INTEGER DEFAULT 0 NOT NULL CHECK(reg_addr >= 0 AND reg_addr <= 65534),
|
"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"),
|
PRIMARY KEY("id"),
|
||||||
FOREIGN KEY("slave_id") REFERENCES modbus_slaves("id") ON DELETE CASCADE
|
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),
|
(3, "Device #3", 8, 16, "Label 3", 0),
|
||||||
(4, "Device #4", NULL, NULL, "Label 4", 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
|
-- Down
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|||||||
@ -632,16 +632,16 @@ video {
|
|||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.my-4 {
|
|
||||||
margin-top: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.my-3 {
|
.my-3 {
|
||||||
margin-top: 0.75rem;
|
margin-top: 0.75rem;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.my-4 {
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.mb-2 {
|
.mb-2 {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
@ -662,10 +662,6 @@ video {
|
|||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mr-auto {
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-1 {
|
.mt-1 {
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
}
|
}
|
||||||
@ -686,10 +682,6 @@ 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;
|
||||||
}
|
}
|
||||||
@ -706,10 +698,6 @@ video {
|
|||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mt-auto {
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block {
|
.block {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@ -750,6 +738,10 @@ video {
|
|||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.min-h-\[140px\] {
|
||||||
|
min-height: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
.min-h-\[50px\] {
|
.min-h-\[50px\] {
|
||||||
min-height: 50px;
|
min-height: 50px;
|
||||||
}
|
}
|
||||||
@ -766,31 +758,10 @@ video {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.w-16 {
|
|
||||||
width: 4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-24 {
|
|
||||||
width: 6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-52 {
|
|
||||||
width: 13rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.min-w-\[256px\] {
|
.min-w-\[256px\] {
|
||||||
min-width: 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-w-2xl {
|
||||||
max-width: 42rem;
|
max-width: 42rem;
|
||||||
}
|
}
|
||||||
@ -799,10 +770,6 @@ video {
|
|||||||
max-width: 64rem;
|
max-width: 64rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.max-w-md {
|
|
||||||
max-width: 28rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-1 {
|
.flex-1 {
|
||||||
flex: 1 1 0%;
|
flex: 1 1 0%;
|
||||||
}
|
}
|
||||||
@ -811,10 +778,6 @@ video {
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.basis-52 {
|
|
||||||
flex-basis: 13rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transform {
|
.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));
|
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;
|
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-cols-1 {
|
||||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
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-row {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
@ -891,6 +822,10 @@ video {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.items-start {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
.items-center {
|
.items-center {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@ -907,10 +842,6 @@ video {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gap-3 {
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gap-2 {
|
.gap-2 {
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
@ -939,10 +870,6 @@ video {
|
|||||||
margin-bottom: calc(0.75rem * var(--tw-space-y-reverse));
|
margin-bottom: calc(0.75rem * var(--tw-space-y-reverse));
|
||||||
}
|
}
|
||||||
|
|
||||||
.overflow-auto {
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overflow-y-auto {
|
.overflow-y-auto {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
@ -951,10 +878,6 @@ video {
|
|||||||
border-radius: 0.125rem;
|
border-radius: 0.125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rounded-md {
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.border {
|
.border {
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
}
|
}
|
||||||
@ -993,11 +916,6 @@ video {
|
|||||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
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;
|
||||||
}
|
}
|
||||||
@ -1044,19 +962,13 @@ 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 {
|
.pl-0 {
|
||||||
padding-top: 0.875rem;
|
padding-left: 0px;
|
||||||
padding-bottom: 0.875rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pl-2 {
|
.pl-2 {
|
||||||
@ -1071,10 +983,6 @@ video {
|
|||||||
padding-top: 1.25rem;
|
padding-top: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pl-0 {
|
|
||||||
padding-left: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-left {
|
.text-left {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
@ -1098,6 +1006,11 @@ video {
|
|||||||
line-height: 2.5rem;
|
line-height: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-5xl {
|
||||||
|
font-size: 3rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.text-lg {
|
.text-lg {
|
||||||
font-size: 1.125rem;
|
font-size: 1.125rem;
|
||||||
line-height: 1.75rem;
|
line-height: 1.75rem;
|
||||||
@ -1113,11 +1026,6 @@ video {
|
|||||||
line-height: 1.75rem;
|
line-height: 1.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-5xl {
|
|
||||||
font-size: 3rem;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.font-bold {
|
.font-bold {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
@ -1221,6 +1129,12 @@ select {
|
|||||||
padding-bottom: 0.25rem;
|
padding-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
margin: 0px;
|
||||||
|
width: auto;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
padding-left: 0.375rem;
|
padding-left: 0.375rem;
|
||||||
padding-right: 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: 768px) {
|
||||||
@media (min-width: 640px) {
|
@media (min-width: 640px) {
|
||||||
.md\:sm\:grid-cols-2 {
|
.md\:sm\:grid-cols-2 {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.md\:grid-cols-2 {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
@ -1277,8 +1181,4 @@ button:disabled {
|
|||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
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;
|
@apply w-full border py-1 rounded-sm my-0.5 bg-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
@apply w-auto m-0 align-middle;
|
||||||
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
@apply px-1.5;
|
@apply px-1.5;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,10 +20,6 @@
|
|||||||
<!-- Monitoring content -->
|
<!-- Monitoring content -->
|
||||||
<template x-if="$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'">
|
<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="flex h-full flex-row space-x-2">
|
||||||
<div class="border-r min-w-[256px] p-2.5 pl-0 pt-5">
|
<div class="border-r min-w-[256px] p-2.5 pl-0 pt-5">
|
||||||
<div class="flex flex-col space-y-1">
|
<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">
|
<span x-show="devices.length === 0" class="text-gray-500 text-sm text-center block">
|
||||||
No devices added.
|
No devices added.
|
||||||
</span>
|
</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">
|
<template x-for="device in devices" :key="device.id">
|
||||||
<button
|
<button
|
||||||
@click="selectDevice(device)"
|
@click="selectDevice(device)"
|
||||||
:class="{'bg-gray-100': selectedDevice?.id === device.id}"
|
: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>
|
<span x-text="device.name" class="text-md font-bold"></span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
@ -46,14 +41,15 @@
|
|||||||
<!-- MODAL: Create new device -->
|
<!-- MODAL: Create new device -->
|
||||||
<template x-teleport="body">
|
<template x-teleport="body">
|
||||||
<div
|
<div
|
||||||
@keyup.escape.window="modalCreateDevice.close()"
|
x-data="deviceModal"
|
||||||
|
@open-create-device-modal.window="open()"
|
||||||
|
@keyup.escape.window="close()"
|
||||||
class="backdrop z-50"
|
class="backdrop z-50"
|
||||||
x-show="modalCreateDevice.isOpen"
|
x-show="isOpen">
|
||||||
>
|
|
||||||
<div class="modal">
|
<div class="modal">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<h2 class="font-bold text-lg">Create new device</h2>
|
<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">
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
@ -63,30 +59,60 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<form x-on:submit.prevent="createDevice" action="#">
|
<form x-data="{ graphView: '' }" @submit.prevent="create(() => graphView = 'none')" action="#">
|
||||||
<div class="flex flex-col space-y-2">
|
<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>
|
<div>
|
||||||
<label for="id">Slave Id</label>
|
<label :for="$id('id')"><span class="text-red-500">*</span>Slave ID:</label>
|
||||||
<input placeholder="1" id="id" name="id" type="number" min="1" max="255" required />
|
<input x-model.number="data.id" :id="$id('id')" name="id" placeholder="1" type="number" min="1" max="255" required />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="id">Name</label>
|
<label :for="$id('name')"><span class="text-red-500">*</span>Name:</label>
|
||||||
<input placeholder="Device #1" id="id" name="id" type="text" required />
|
<input x-model="data.name" placeholder="Device #1" :id="$id('name')" name="name" type="text" required />
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</form>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<!-- Create new device -->
|
<!-- Create new device -->
|
||||||
<button
|
<button
|
||||||
@click="modalCreateDevice.open()"
|
@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"
|
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>
|
<span class="text-4xl text-gray-400 select-none">+</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -107,25 +133,25 @@
|
|||||||
<div class="flex-auto pl-2">
|
<div class="flex-auto pl-2">
|
||||||
<div x-show="selectedDevice != null">
|
<div x-show="selectedDevice != null">
|
||||||
<div class="pt-5 border-b flex flex-wrap justify-between items-center pr-6">
|
<div class="pt-5 border-b flex flex-wrap justify-between items-center pr-6">
|
||||||
|
<div class="mb-2">
|
||||||
<h2 x-text="selectedDevice?.name" class="font-bold text-3xl my-2 mr-2"></h2>
|
<h2 x-text="selectedDevice?.name" class="font-bold text-3xl my-2 mr-2"></h2>
|
||||||
<!-- Device create, delete -->
|
<span>ID: </span><span x-text="selectedDevice?.id"></span>
|
||||||
|
</div>
|
||||||
|
<!-- Device create, delete buttons -->
|
||||||
<div>
|
<div>
|
||||||
<a href="#">Edit</a>
|
<a href="#">Edit</a>
|
||||||
<a
|
<a @click="$dispatch('dialog', {
|
||||||
@click="$dispatch('dialog', {
|
|
||||||
title: 'Remove device',
|
title: 'Remove device',
|
||||||
body: `Remove ${selectedDevice.name}? All logs will be cleared.`,
|
body: `Remove ${selectedDevice.name}? All logs will be cleared.`,
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
onConfirm: async () => await removeDevice(selectedDevice.id)
|
onConfirm: async () => await removeDevice(selectedDevice.id)
|
||||||
});"
|
});"
|
||||||
class="text-red-500"
|
class="text-red-500"
|
||||||
href="#"
|
href="#">
|
||||||
>
|
|
||||||
Delete
|
Delete
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Display Graph -->
|
<!-- Display Graph -->
|
||||||
<div x-show="selectedDevice?.g_display_reg_addr != null" class="mt-4">
|
<div x-show="selectedDevice?.g_display_reg_addr != null" class="mt-4">
|
||||||
<div id="div_g" style="width: 720px; height: 300px"></div>
|
<div id="div_g" style="width: 720px; height: 300px"></div>
|
||||||
@ -133,13 +159,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- Display values -->
|
<!-- Display values -->
|
||||||
<h2 class="text-xl font-bold mb-3 mt-5">Display values</h2>
|
<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">
|
<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="border-2 p-2.5 rounded-sm">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between items-start">
|
||||||
<h3 class="text-xl font-bold">CV1</h3>
|
<h3 x-text="value.name" class="text-xl font-bold"></h3>
|
||||||
<div>
|
<div class="flex">
|
||||||
|
<!-- Edit display value -->
|
||||||
<button>
|
<button>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
@ -148,7 +175,13 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</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">
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
fill="#ef4444"
|
fill="#ef4444"
|
||||||
@ -160,14 +193,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span>Value: 7892</span>
|
<span>Value: ---</span>
|
||||||
<span>Reg Addr: 6</span>
|
<span>Reg Addr: <span x-text="value.reg_addr"></span></span>
|
||||||
<span>Format: 32</span>
|
<span>Format: <span x-text="value.reg_format"></span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div
|
<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>
|
<span class="text-5xl text-gray-400 font-bold select-none">+</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -52,23 +52,58 @@ document.addEventListener("alpine:init", async () => {
|
|||||||
this.currentPage = "404";
|
this.currentPage = "404";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.currentPage = page;
|
this.currentPage = page;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
Alpine.data("monitoringPage", () => ({
|
Alpine.data('deviceModal', () => ({
|
||||||
selectedDevice: null,
|
|
||||||
devices: [],
|
|
||||||
modalCreateDevice: {
|
|
||||||
isOpen: false,
|
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() {
|
open() {
|
||||||
this.isOpen = true;
|
this.isOpen = true;
|
||||||
},
|
},
|
||||||
close() {
|
close() {
|
||||||
this.isOpen = false;
|
this.isOpen = false;
|
||||||
},
|
},
|
||||||
},
|
}))
|
||||||
|
|
||||||
|
Alpine.data("monitoringPage", () => ({
|
||||||
|
selectedDevice: null,
|
||||||
|
devices: [],
|
||||||
async init() {
|
async init() {
|
||||||
try {
|
try {
|
||||||
this.$store.app.currentPage = "loading";
|
this.$store.app.currentPage = "loading";
|
||||||
@ -92,7 +127,7 @@ document.addEventListener("alpine:init", async () => {
|
|||||||
},
|
},
|
||||||
selectDevice(device) {
|
selectDevice(device) {
|
||||||
if (device.id === this.selectedDevice?.id) return;
|
if (device.id === this.selectedDevice?.id) return;
|
||||||
graphData = [];
|
graphData = []; // todo: reset graph
|
||||||
this.selectedDevice = device;
|
this.selectedDevice = device;
|
||||||
},
|
},
|
||||||
async removeDevice(id) {
|
async removeDevice(id) {
|
||||||
@ -104,6 +139,14 @@ document.addEventListener("alpine:init", async () => {
|
|||||||
console.error("Error while removing device.", error);
|
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", () => ({
|
Alpine.data("configAppPage", () => ({
|
||||||
|
|||||||
@ -9,3 +9,9 @@ export function objKeysExclude(obj, keys) {
|
|||||||
Object.keys(res).forEach(k => keys.includes(k) && delete res[k]);
|
Object.keys(res).forEach(k => keys.includes(k) && delete res[k]);
|
||||||
return res;
|
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 express from "express";
|
||||||
import { modbusRouter } from './modbus.router.js'
|
import { modbusRouter } from './modbus.router.js'
|
||||||
import { appConfigSchema } from "#root/validators/index.js";
|
import { appRouter } from "./app.router.js";
|
||||||
import { appController } from "#root/controllers/index.js";
|
|
||||||
import { validateBodyMiddleware } from "#root/middlewares/index.js";
|
|
||||||
|
|
||||||
const apiRouter = express.Router();
|
const apiRouter = express.Router();
|
||||||
|
|
||||||
apiRouter.use("/modbus", modbusRouter);
|
apiRouter.use("/modbus", modbusRouter);
|
||||||
apiRouter.get("/app/config", appController.getConfig);
|
apiRouter.use('/app', appRouter)
|
||||||
apiRouter.patch("/app/config", validateBodyMiddleware(appConfigSchema), appController.updateConfig);
|
|
||||||
apiRouter.get("/app/is_configured", appController.isConfigured);
|
|
||||||
|
|
||||||
export { apiRouter };
|
export { apiRouter };
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import { modbusController } from "#root/controllers/index.js"
|
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";
|
import { validateBodyMiddleware } from "#root/middlewares/index.js";
|
||||||
|
|
||||||
const modbusRouter = express.Router();
|
const modbusRouter = express.Router();
|
||||||
@ -9,6 +9,7 @@ modbusRouter.get("/devices", modbusController.list);
|
|||||||
modbusRouter.post("/create_device", validateBodyMiddleware(modbusDeviceSchema), modbusController.createDevice)
|
modbusRouter.post("/create_device", validateBodyMiddleware(modbusDeviceSchema), modbusController.createDevice)
|
||||||
modbusRouter.post('/remove_device', validateBodyMiddleware(removeModbusDeviceSchema), modbusController.removeDevice);
|
modbusRouter.post('/remove_device', validateBodyMiddleware(removeModbusDeviceSchema), modbusController.removeDevice);
|
||||||
modbusRouter.post('/update_device', validateBodyMiddleware(modbusDeviceSchema), modbusController.updateDevice);
|
modbusRouter.post('/update_device', validateBodyMiddleware(modbusDeviceSchema), modbusController.updateDevice);
|
||||||
|
modbusRouter.post('/remove_display-value', validateBodyMiddleware(removeDisplayValueSchema), modbusController.removeDisplayValue);
|
||||||
modbusRouter.post("/connect", modbusController.connect);
|
modbusRouter.post("/connect", modbusController.connect);
|
||||||
modbusRouter.post("/close", modbusController.close);
|
modbusRouter.post("/close", modbusController.close);
|
||||||
modbusRouter.get("/data_stream", modbusController.streamData);
|
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']);
|
||||||
@ -18,3 +18,4 @@ export const appConfigSchema = object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
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({
|
export const modbusDeviceSchema = object({
|
||||||
id: number().required(),
|
id: number().required(),
|
||||||
name: string().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_display_reg_format: number().oneOf([16, 32]).nullable(),
|
||||||
g_y_label: string().max(40).nullable(),
|
g_y_label: string().max(40).nullable(),
|
||||||
is_logging: boolean().required()
|
is_logging: boolean().required()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user