This commit is contained in:
Sergey Yarkov 2023-10-27 16:26:38 +04:00
parent 0e10984830
commit d934e93d93
14 changed files with 174 additions and 104 deletions

View File

@ -4,7 +4,7 @@ import pino from "pino-http";
/**
* Routers
*/
import { apiRouter } from "#root/routes/api.router.js";
import { apiRouter } from "#root/routes/index.js";
/**
* Middlewares

View File

@ -1,7 +1,4 @@
import Modbus from "modbus-serial";
const modbusClient = new Modbus();
modbusClient.on("close", () => console.log("Modbus connection closed."));
export default modbusClient;

View File

@ -51,6 +51,7 @@ export const modbusController = {
if (!modbusClient.isOpen) return res.status(400).json({ message: "Already closed." });
modbusClient.close(undefined);
modbusClient.destroy(undefined);
return res.status(200).json({ message: 'Closed.' });
} catch (error) {
next(error);
}
@ -77,7 +78,22 @@ export const modbusController = {
* @param {import('express').Response} res
* @param {import('express').NextFunction} next
*/
async createSlaveDevice(req, res, next) {},
async createDevice(req, res, next) {
try {
await db.run(`INSERT INTO "modbus_slaves" (
"id",
"name",
"g_display_reg_addr",
"g_display_reg_format",
"g_y_label",
"is_logging")
VALUES (?, ?, ?, ?, ?, ?)`,
[...req.body]);
return res.send(200).json({ message: 'Device created.', data: req.body });
} catch (error) {
next(error)
}
},
/**
* Remove modbus slave device
@ -85,7 +101,21 @@ export const modbusController = {
* @param {import('express').Response} res
* @param {import('express').NextFunction} next
*/
async removeSlaveDevice(req, res, next) {
async updateDevice(req, res, next) {
try {
} catch (error) {
next(error);
}
},
/**
* Remove modbus slave device
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {import('express').NextFunction} next
*/
async removeDevice(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.' });

BIN
data.db

Binary file not shown.

7
index.d.ts vendored
View File

@ -10,10 +10,3 @@ export type AppConfig = {
log_interval_ms: number;
};
export type SlaveModbusDevice = {
id: number;
name: string;
display_reg_addr: number | null;
display_reg_format: number | null;
is_logging: boolean;
}

View File

@ -682,6 +682,18 @@ video {
margin-top: 1.5rem;
}
.mb-1 {
margin-bottom: 0.25rem;
}
.mb-2 {
margin-bottom: 0.5rem;
}
.mb-10 {
margin-bottom: 2.5rem;
}
.block {
display: block;
}
@ -714,8 +726,12 @@ video {
height: 100vh;
}
.max-h-\[520px\] {
max-height: 520px;
.max-h-\[400px\] {
max-height: 400px;
}
.min-h-\[50px\] {
min-height: 50px;
}
.w-1 {

View File

@ -27,31 +27,34 @@
<div class="flex h-full flex-row space-x-2">
<div class="border-r min-w-[256px] p-2.5 pt-5">
<div class="flex flex-col space-y-1">
<!-- List of devices -->
<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">
<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">
<template x-for="device in devices">
<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"
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>
</div>
<button class="w-full text-center border-2 border-dashed rounded-sm hover:bg-gray-50 cursor-pointer">
<!-- Add new device -->
<button 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>
<hr class="my-4" />
<!-- Connection -->
<div>
<span class="italic text-sm">Connection:</span>
<div class="flex flex-col mt-1.5">
<span class="text-sm"
>Status:
<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>
@ -63,20 +66,27 @@
<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>
<!-- Device create, delete -->
<div>
<a href="#">Edit</a>
<a
@click="$store.confirmDialog.open('Remove device', `Delete ${selectedDevice.name}? All logs will be cleared.`, removeDevice, selectedDevice.id)"
<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="#"
>Delete</a
>
href="#">
Delete
</a>
</div>
</div>
<!-- Display Graph -->
<div class="mt-4">
<div id="div_g" x-ref="graph" style="width: 720px; height: 300px"></div>
</div>
<hr class="mt-10" />
<!-- Display values -->
<h2 class="text-xl font-bold mb-3">Display values</h2>
</div>
<div class="select-none" x-show="selectedDevice === null">
@ -90,7 +100,7 @@
<!-- Configuration content -->
<template x-if="$store.app.currentPage === 'configuration'">
<main class="mt-6" x-data="configAppPage" x-bind:hidden="$store.app.currentPage !== 'configuration'">
<main class="mt-6 mb-2" x-data="configAppPage" x-bind:hidden="$store.app.currentPage !== 'configuration'">
<div class="border rounded-sm p-4 max-w-2xl mx-auto">
<h2 class="text-2xl font-bold">Setup configuration</h2>
<p class="text-gray-700 italic">
@ -295,16 +305,17 @@
</main>
</template>
<!-- Confirm dialog -->
<div x-show="$store.confirmDialog.isOpen" class="backdrop z-50">
<!-- Dialog -->
<div x-data="dialog">
<template @dialog.window="open($event)" x-if="isOpen">
<div 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">
<h2 x-text="labels.title" class="font-bold text-lg"></h2>
<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"
@ -314,13 +325,15 @@
</button>
</div>
<hr />
<p x-text="$store.confirmDialog.labels.body"></p>
<p x-text="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>
<button x-bind:disabled="isLoading" x-bind:class="isLoading && 'opacity-70'" @click="confirm()" class="btn">Confirm</button>
<button @click="close()" class="btn bg-red-500">Cancel</button>
</div>
</div>
</div>
</template>
</div>
</div>
</body>
</html>

View File

@ -68,7 +68,7 @@ document.addEventListener("alpine:init", async () => {
showRoller: false,
ylabel: "Y Label",
valueRange: [0.0, 1.8],
//labels: ["Time", "Random"],
// labels: ["Time", "Random"],
});
this.devices = await api.get("/modbus/devices");
window.intervalId = setInterval(() => {
@ -86,10 +86,10 @@ document.addEventListener("alpine:init", async () => {
graphData = [];
this.selectedDevice = device;
},
async removeDevice(id, ctx) {
async removeDevice(id) {
try {
//await api.post('/modbus/remove_device', { id });
this.devices = _this.devices.filter((d) => d.id != id);
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);
@ -128,28 +128,37 @@ document.addEventListener("alpine:init", async () => {
},
}));
Alpine.store("confirmDialog", {
Alpine.data("dialog", () => ({
isOpen: false,
isLoading: 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;
},
cb: null,
onConfirm: () => undefined,
onError: () => undefined,
confirm() {
this.cb.fn && this.cb.fn(...this.cb.args);
if (this.onConfirm.constructor.name === 'AsyncFunction') {
this.isLoading = true;
this.onConfirm()
.then(() => this.close())
.catch((e) => this.onError(e))
.finally(() => this.isLoading = false)
} else {
this.onConfirm();
this.close();
}
},
close() {
this.isOpen = false;
this.cb = null;
},
});
open(event) {
const { detail } = event;
this.isOpen = detail.isOpen;
this.onConfirm = detail.onConfirm;
this.labels = { title: detail.title, body: detail.body };
}
}));
});

View File

@ -1,31 +0,0 @@
import express from "express";
/**
* Validators
*/
import { appConfigSchema, removeModbusDeviceSchema } from "#root/validators/index.js";
/**
* Controllers
*/
import { appController, modbusController } from "#root/controllers/index.js";
/**
* Middlewares
*/
import { validateBodyMiddleware } from "#root/middlewares/index.js";
const apiRouter = express.Router();
apiRouter.get("/app/config", appController.getConfig);
apiRouter.patch("/app/config", validateBodyMiddleware(appConfigSchema), appController.updateConfig);
apiRouter.get("/app/is_configured", appController.isConfigured);
apiRouter.get("/modbus/devices", modbusController.list);
apiRouter.post('/modbus/remove_device', validateBodyMiddleware(removeModbusDeviceSchema), modbusController.removeSlaveDevice);
apiRouter.post("/modbus/connect", modbusController.connect);
apiRouter.post("/modbus/close", modbusController.close);
apiRouter.get("/modbus/data_stream", modbusController.streamData);
apiRouter.get('/modbus/status', modbusController.status);
export { apiRouter };

14
routes/api/index.js Normal file
View File

@ -0,0 +1,14 @@
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";
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);
export { apiRouter };

View File

@ -0,0 +1,17 @@
import express from "express";
import { modbusController } from "#root/controllers/index.js"
import { modbusDeviceSchema, removeModbusDeviceSchema } from "#root/validators/index.js";
import { validateBodyMiddleware } from "#root/middlewares/index.js";
const modbusRouter = express.Router();
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("/connect", modbusController.connect);
modbusRouter.post("/close", modbusController.close);
modbusRouter.get("/data_stream", modbusController.streamData);
modbusRouter.get('/status', modbusController.status);
export { modbusRouter }

View File

@ -1 +1 @@
export * from './api.router.js';
export * from './api/index.js';

View File

@ -17,4 +17,4 @@ export const appConfigSchema = object({
log_interval_ms: number().min(1000).required(),
});
export const removeModbusDeviceSchema = object({ id: number().max(255).min(1).required() })
export * from './modbus-device.schema.js'

View File

@ -0,0 +1,12 @@
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_format: number().oneOf([16, 32]).nullable(),
g_y_label: string().max(40).nullable(),
is_logging: boolean().required()
});
export const removeModbusDeviceSchema = modbusDeviceSchema.pick(['id']);