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
0e10984830
commit
d934e93d93
@ -4,7 +4,7 @@ import pino from "pino-http";
|
|||||||
/**
|
/**
|
||||||
* Routers
|
* Routers
|
||||||
*/
|
*/
|
||||||
import { apiRouter } from "#root/routes/api.router.js";
|
import { apiRouter } from "#root/routes/index.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Middlewares
|
* Middlewares
|
||||||
|
|||||||
@ -1,7 +1,4 @@
|
|||||||
import Modbus from "modbus-serial";
|
import Modbus from "modbus-serial";
|
||||||
|
|
||||||
const modbusClient = new Modbus();
|
const modbusClient = new Modbus();
|
||||||
|
|
||||||
modbusClient.on("close", () => console.log("Modbus connection closed."));
|
|
||||||
|
|
||||||
export default modbusClient;
|
export default modbusClient;
|
||||||
|
|||||||
@ -51,6 +51,7 @@ export const modbusController = {
|
|||||||
if (!modbusClient.isOpen) return res.status(400).json({ message: "Already closed." });
|
if (!modbusClient.isOpen) return res.status(400).json({ message: "Already closed." });
|
||||||
modbusClient.close(undefined);
|
modbusClient.close(undefined);
|
||||||
modbusClient.destroy(undefined);
|
modbusClient.destroy(undefined);
|
||||||
|
return res.status(200).json({ message: 'Closed.' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
@ -77,7 +78,22 @@ export const modbusController = {
|
|||||||
* @param {import('express').Response} res
|
* @param {import('express').Response} res
|
||||||
* @param {import('express').NextFunction} next
|
* @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
|
* Remove modbus slave device
|
||||||
@ -85,7 +101,21 @@ export const modbusController = {
|
|||||||
* @param {import('express').Response} res
|
* @param {import('express').Response} res
|
||||||
* @param {import('express').NextFunction} next
|
* @param {import('express').NextFunction} next
|
||||||
*/
|
*/
|
||||||
async removeSlaveDevice(req, res, next) {
|
async 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 {
|
try {
|
||||||
const device = await db.get(`SELECT id FROM "modbus_slaves" WHERE id = ?`, [req.body.id]);
|
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.' });
|
if (!device) return res.status(404).json({ message: 'Device not found.' });
|
||||||
|
|||||||
7
index.d.ts
vendored
7
index.d.ts
vendored
@ -10,10 +10,3 @@ export type AppConfig = {
|
|||||||
log_interval_ms: number;
|
log_interval_ms: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SlaveModbusDevice = {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
display_reg_addr: number | null;
|
|
||||||
display_reg_format: number | null;
|
|
||||||
is_logging: boolean;
|
|
||||||
}
|
|
||||||
@ -682,6 +682,18 @@ video {
|
|||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mb-1 {
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-2 {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-10 {
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.block {
|
.block {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@ -714,8 +726,12 @@ video {
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.max-h-\[520px\] {
|
.max-h-\[400px\] {
|
||||||
max-height: 520px;
|
max-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.min-h-\[50px\] {
|
||||||
|
min-height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.w-1 {
|
.w-1 {
|
||||||
|
|||||||
@ -27,31 +27,34 @@
|
|||||||
<div class="flex h-full flex-row space-x-2">
|
<div class="flex h-full flex-row space-x-2">
|
||||||
<div class="border-r min-w-[256px] p-2.5 pt-5">
|
<div class="border-r min-w-[256px] p-2.5 pt-5">
|
||||||
<div class="flex flex-col space-y-1">
|
<div class="flex flex-col space-y-1">
|
||||||
|
<!-- List of devices -->
|
||||||
<span class="italic text-sm">Devices:</span>
|
<span class="italic text-sm">Devices:</span>
|
||||||
<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.</span
|
No devices added.
|
||||||
>
|
</span>
|
||||||
<div class="flex flex-col max-h-[520px] overflow-y-auto space-y-2">
|
<div class="flex flex-col max-h-[400px] overflow-y-auto space-y-2">
|
||||||
<template x-for="device in devices">
|
<template x-for="device in devices">
|
||||||
<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 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>
|
||||||
</div>
|
</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>
|
<span class="text-4xl text-gray-400 select-none">+</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<hr class="my-4" />
|
<hr class="my-4" />
|
||||||
|
<!-- Connection -->
|
||||||
<div>
|
<div>
|
||||||
<span class="italic text-sm">Connection:</span>
|
<span class="italic text-sm">Connection:</span>
|
||||||
<div class="flex flex-col mt-1.5">
|
<div class="flex flex-col mt-1.5">
|
||||||
<span class="text-sm"
|
<span class="text-sm">
|
||||||
>Status:
|
Status:
|
||||||
<b x-show="$store.app.is_opened" class="text-green-700">Opened</b>
|
<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>
|
<b x-show="!$store.app.is_opened" class="text-red-700">Closed</b>
|
||||||
</span>
|
</span>
|
||||||
@ -63,20 +66,27 @@
|
|||||||
<div x-show="selectedDevice != null">
|
<div x-show="selectedDevice != null">
|
||||||
<div class="pt-5 border-b flex justify-between items-center pr-6">
|
<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>
|
<h2 x-text="selectedDevice?.name" class="font-bold text-3xl my-2"></h2>
|
||||||
|
<!-- Device create, delete -->
|
||||||
<div>
|
<div>
|
||||||
<a href="#">Edit</a>
|
<a href="#">Edit</a>
|
||||||
<a
|
<a @click="$dispatch('dialog', {
|
||||||
@click="$store.confirmDialog.open('Remove device', `Delete ${selectedDevice.name}? All logs will be cleared.`, removeDevice, selectedDevice.id)"
|
title: 'Remove device',
|
||||||
|
body: `Remove ${selectedDevice.name}? All logs will be cleared.`,
|
||||||
|
isOpen: true,
|
||||||
|
onConfirm: async () => await removeDevice(selectedDevice.id)
|
||||||
|
});"
|
||||||
class="text-red-500"
|
class="text-red-500"
|
||||||
href="#"
|
href="#">
|
||||||
>Delete</a
|
Delete
|
||||||
>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Display Graph -->
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<div id="div_g" x-ref="graph" style="width: 720px; height: 300px"></div>
|
<div id="div_g" x-ref="graph" style="width: 720px; height: 300px"></div>
|
||||||
</div>
|
</div>
|
||||||
<hr class="mt-10" />
|
<hr class="mt-10" />
|
||||||
|
<!-- Display values -->
|
||||||
<h2 class="text-xl font-bold mb-3">Display values</h2>
|
<h2 class="text-xl font-bold mb-3">Display values</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="select-none" x-show="selectedDevice === null">
|
<div class="select-none" x-show="selectedDevice === null">
|
||||||
@ -90,7 +100,7 @@
|
|||||||
|
|
||||||
<!-- Configuration content -->
|
<!-- Configuration content -->
|
||||||
<template x-if="$store.app.currentPage === 'configuration'">
|
<template x-if="$store.app.currentPage === 'configuration'">
|
||||||
<main class="mt-6" x-data="configAppPage" x-bind:hidden="$store.app.currentPage !== 'configuration'">
|
<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">
|
<div class="border rounded-sm p-4 max-w-2xl mx-auto">
|
||||||
<h2 class="text-2xl font-bold">Setup configuration</h2>
|
<h2 class="text-2xl font-bold">Setup configuration</h2>
|
||||||
<p class="text-gray-700 italic">
|
<p class="text-gray-700 italic">
|
||||||
@ -295,16 +305,17 @@
|
|||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Confirm dialog -->
|
<!-- Dialog -->
|
||||||
<div x-show="$store.confirmDialog.isOpen" class="backdrop z-50">
|
<div x-data="dialog">
|
||||||
|
<template @dialog.window="open($event)" x-if="isOpen">
|
||||||
|
<div class="backdrop z-50">
|
||||||
<div
|
<div
|
||||||
role="dialog"
|
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"
|
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">
|
<div class="flex justify-between items-center">
|
||||||
<h2 x-text="$store.confirmDialog.labels.title" class="font-bold text-lg"></h2>
|
<h2 x-text="labels.title" class="font-bold text-lg"></h2>
|
||||||
<button @click="$store.confirmDialog.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"
|
||||||
@ -314,13 +325,15 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<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">
|
<div class="mt-4 flex justify-end space-x-2">
|
||||||
<button @click="$store.confirmDialog.confirm()" class="btn">Confirm</button>
|
<button x-bind:disabled="isLoading" x-bind:class="isLoading && 'opacity-70'" @click="confirm()" class="btn">Confirm</button>
|
||||||
<button @click="$store.confirmDialog.close()" class="btn bg-red-500">Cancel</button>
|
<button @click="close()" class="btn bg-red-500">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -86,10 +86,10 @@ document.addEventListener("alpine:init", async () => {
|
|||||||
graphData = [];
|
graphData = [];
|
||||||
this.selectedDevice = device;
|
this.selectedDevice = device;
|
||||||
},
|
},
|
||||||
async removeDevice(id, ctx) {
|
async removeDevice(id) {
|
||||||
try {
|
try {
|
||||||
//await api.post('/modbus/remove_device', { id });
|
await api.post('/modbus/remove_device', { id });
|
||||||
this.devices = _this.devices.filter((d) => d.id != id);
|
this.devices = this.devices.filter((d) => d.id != id);
|
||||||
this.selectedDevice = null;
|
this.selectedDevice = null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error while removing device.", 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,
|
isOpen: false,
|
||||||
|
isLoading: false,
|
||||||
labels: {
|
labels: {
|
||||||
title: "Confirm dialog",
|
title: "Confirm dialog",
|
||||||
body: "Are you sure you want to perform this action?",
|
body: "Are you sure you want to perform this action?",
|
||||||
},
|
},
|
||||||
cb: {
|
cb: null,
|
||||||
fn: null,
|
onConfirm: () => undefined,
|
||||||
args: [],
|
onError: () => undefined,
|
||||||
},
|
|
||||||
open(title, body, cb, ...args) {
|
|
||||||
this.isOpen = true;
|
|
||||||
this.labels = { title, body };
|
|
||||||
this.cb.fn = cb;
|
|
||||||
this.cb.args = args;
|
|
||||||
},
|
|
||||||
confirm() {
|
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();
|
this.close();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
close() {
|
close() {
|
||||||
this.isOpen = false;
|
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 };
|
||||||
|
}
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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
14
routes/api/index.js
Normal 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 };
|
||||||
17
routes/api/modbus.router.js
Normal file
17
routes/api/modbus.router.js
Normal 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 }
|
||||||
@ -1 +1 @@
|
|||||||
export * from './api.router.js';
|
export * from './api/index.js';
|
||||||
|
|||||||
@ -17,4 +17,4 @@ export const appConfigSchema = object({
|
|||||||
log_interval_ms: number().min(1000).required(),
|
log_interval_ms: number().min(1000).required(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const removeModbusDeviceSchema = object({ id: number().max(255).min(1).required() })
|
export * from './modbus-device.schema.js'
|
||||||
12
validators/modbus-device.schema.js
Normal file
12
validators/modbus-device.schema.js
Normal 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']);
|
||||||
Loading…
x
Reference in New Issue
Block a user