This commit is contained in:
Sergey Yarkov 2023-10-30 16:26:56 +03:00
parent 8a0cfe5063
commit 90271dc45f
15 changed files with 265 additions and 196 deletions

View File

@ -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

BIN
data.db

Binary file not shown.

19
getslaves.sql Normal file
View 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;

View File

@ -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
--------------------------------------------------------------------------------

View File

@ -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));
}
}

View File

@ -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;
}

View File

@ -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>

View File

@ -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", () => ({

View File

@ -9,3 +9,9 @@ export function objKeysExclude(obj, keys) {
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
View 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 }

View File

@ -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 };

View File

@ -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);

View 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']);

View File

@ -18,3 +18,4 @@ export const appConfigSchema = object({
});
export * from './modbus-device.schema.js'
export * from './display-value.schema.js'

View File

@ -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()