This commit is contained in:
Sergey Yarkov 2023-11-08 16:26:04 +03:00
parent 582cbd285e
commit da4cd16ade
9 changed files with 118 additions and 33 deletions

View File

@ -1,6 +1,7 @@
import db from "#root/config/database.config.js"; import db from "#root/config/database.config.js";
import modbusClient from "#root/config/modbus-client.config.js"; import modbusClient from "#root/config/modbus-client.config.js";
import * as utils from '#root/utils/index.js' import * as utils from '#root/utils/index.js'
import { RowNotFoundError } from '#root/errors/index.js'
import { appService, modbusService } from "#root/services/index.js"; import { appService, modbusService } from "#root/services/index.js";
export const modbusController = { export const modbusController = {
@ -136,7 +137,7 @@ export const modbusController = {
async removeDevice(req, res, 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) throw new RowNotFoundError();
await db.run(`DELETE FROM "modbus_slaves" WHERE id = ?`, [req.body.id]); await db.run(`DELETE FROM "modbus_slaves" WHERE id = ?`, [req.body.id]);
return res.status(200).json({ message: "Device removed." }); return res.status(200).json({ message: "Device removed." });
} catch (error) { } catch (error) {
@ -158,7 +159,12 @@ export const modbusController = {
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
`, [name, slave_id, reg_addr, reg_format, reg_type] `, [name, slave_id, reg_addr, reg_format, reg_type]
); );
return res.status(200).json({ message: 'Created.' }); const createdDisplayValue = await db.get(`SELECT * FROM "display_values" WHERE id = last_insert_rowid()`);
return res.status(200).json({
message: 'Created.',
data: createdDisplayValue,
});
} catch (error) { } catch (error) {
next(error); next(error);
} }
@ -179,6 +185,30 @@ export const modbusController = {
} }
}, },
/**
* Update display value
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {import('express').NextFunction} next
*/
async updateDisplayValue(req, res, next) {
try {
const { id, name, slave_id, reg_addr, reg_type, reg_format } = req.body;
const row = await db.get(`SELECT id from "display_values" WHERE id = ?`, [id]);
if (!row) throw new RowNotFoundError();
await db.run(`UPDATE "display_values" SET
name = ?,
slave_id = ?,
reg_addr = ?,
reg_type = ?,
reg_format = ?
WHERE id = ?`, [name, slave_id, reg_addr, reg_type, reg_format, id]);
return res.status(200).json({ message: 'Updated.' });
} catch (error) {
next(error);
}
},
/** /**
* Creates event stream aboud modbus connection status * Creates event stream aboud modbus connection status
* @param {import('express').Request} req * @param {import('express').Request} req
@ -209,7 +239,7 @@ export const modbusController = {
if (!slave_id) return res.status(400).json({ error: { message: "'slave_id' parameter is required." } }); if (!slave_id) return res.status(400).json({ error: { message: "'slave_id' parameter is required." } });
const device = await db.get(`SELECT * FROM "modbus_slaves" WHERE id = ?`, [slave_id]); const device = await db.get(`SELECT * FROM "modbus_slaves" WHERE id = ?`, [slave_id]);
if (!device) return res.status(404).json({ error: { message: "Device not found." } }); if (!device) throw new RowNotFoundError();
utils.setSSEHeaders(res); utils.setSSEHeaders(res);

BIN
data.db

Binary file not shown.

14
errors/index.js Normal file
View File

@ -0,0 +1,14 @@
export class DomainError extends Error {
constructor(message) {
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
export class RowNotFoundError extends DomainError {
constructor() {
super('Row not found.');
this.statusCode = 404;
}
}

View File

@ -9,7 +9,7 @@ export function errorHandlerMiddleware() {
req.log.error(error, "Something went wrong"); req.log.error(error, "Something went wrong");
const status = error.statusCode || 500; const status = error.statusCode || 500;
const message = error.message || "Internal Server Error"; const message = error.message || "Internal Server Error";
res.status(500).json({ res.status(status).json({
error: { error: {
status, status,
message, message,

View File

@ -928,6 +928,11 @@ 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;
} }
@ -987,8 +992,8 @@ video {
padding-left: 0px; padding-left: 0px;
} }
.pl-2 { .pl-\[260px\] {
padding-left: 0.5rem; padding-left: 260px;
} }
.pr-6 { .pr-6 {
@ -999,14 +1004,6 @@ video {
padding-top: 1.25rem; padding-top: 1.25rem;
} }
.pl-\[250px\] {
padding-left: 250px;
}
.pl-\[260px\] {
padding-left: 260px;
}
.text-left { .text-left {
text-align: left; text-align: left;
} }

View File

@ -21,7 +21,7 @@
<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 class="flex h-full flex-row space-x-2"> <div class="flex h-full flex-row space-x-2">
<div class="border-r fixed h-screen min-w-[256px] p-2.5 pl-0 pt-5"> <div class="border-r fixed bg-white h-screen 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">
<!-- List of devices --> <!-- List of devices -->
<span class="italic text-sm">Devices:</span> <span class="italic text-sm">Devices:</span>
@ -290,7 +290,12 @@
<h3 x-text="value.name" class="text-lg font-bold"></h3> <h3 x-text="value.name" class="text-lg font-bold"></h3>
<div class="flex"> <div class="flex">
<!-- Edit display value --> <!-- Edit display value -->
<button> <button @click="$dispatch('open-display-value-modal', {
labels: { title: `Edit ${value.name}`, submit: 'Edit' },
actionType: 'EDIT',
selectedDevice,
displayValue: value
})">
<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="#eab308" fill="#eab308"
@ -330,23 +335,27 @@
</template> </template>
<!-- Create Display Value --> <!-- Create Display Value -->
<button <button
@click="$dispatch('open-create-display-value-modal')" @click="$dispatch('open-display-value-modal', {
labels: { title: 'Create new display value', submit: 'Create' },
actionType: 'CREATE',
selectedDevice,
})"
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" 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>
</button> </button>
<!-- MODAL: Create new display value --> <!-- MODAL: Display value -->
<template x-teleport="body"> <template x-teleport="body">
<div <div
x-data="displayValueModal" x-data="displayValueModal"
@open-create-display-value-modal.window="open()" @open-display-value-modal.window="open($event)"
@keyup.escape.window="close()" @keyup.escape.window="close()"
class="backdrop z-50" class="backdrop z-50"
x-show="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 display value</h2> <h2 x-text="labels.title" class="font-bold text-lg"></h2>
<button @click="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
@ -357,7 +366,7 @@
</button> </button>
</div> </div>
<hr /> <hr />
<form @submit.prevent="create(selectedDevice)" action="#"> <form @submit.prevent="submit" action="#">
<div <div
x-id="['name', 'reg_addr', 'reg_type', 'reg_format']" x-id="['name', 'reg_addr', 'reg_type', 'reg_format']"
class="flex flex-col space-y-2" class="flex flex-col space-y-2"
@ -424,10 +433,10 @@
<button <button
x-bind:disabled="isLoading" x-bind:disabled="isLoading"
x-bind:class="isLoading && 'opacity-70'" x-bind:class="isLoading && 'opacity-70'"
x-text="labels.submit"
type="submit" type="submit"
class="btn" class="btn"
> >
Create
</button> </button>
<button @click="close()" class="btn bg-red-500">Cancel</button> <button @click="close()" class="btn bg-red-500">Cancel</button>
</div> </div>

View File

@ -172,23 +172,44 @@ document.addEventListener("alpine:init", async () => {
reg_type: 'IR', reg_type: 'IR',
reg_format: 'UI16' reg_format: 'UI16'
}, },
labels: {
title: '',
submit: '',
},
selectedDevice: null,
resetDataFields() { resetDataFields() {
this.data.name = ''; this.data.name = '';
this.data.reg_addr = 0; this.data.reg_addr = 0;
this.data.reg_type = 'IR'; this.data.reg_type = 'IR';
this.data.reg_format = 'UI16'; this.data.reg_format = 'UI16';
}, },
async create(selectedDevice) { async create() {
try {
this.isLoading = true; this.isLoading = true;
const data = { const data = { ...this.data, slave_id: this.selectedDevice.id };
...this.data, const result = await api.post('/modbus/create_display-value', data);
slave_id: selectedDevice.id this.selectedDevice?.display_values?.push(result.data);
}
await api.post('/modbus/create_display-value', data);
selectedDevice?.display_values?.push(this.data);
this.resetDataFields(); this.resetDataFields();
this.close(); this.close();
},
async update() {
const data = { ...this.data, slave_id: this.selectedDevice.id };
await api.post('/modbus/update_display-value', data);
this.resetDataFields();
this.close();
},
async submit() {
try {
this.isLoading = true;
switch (this.actionType) {
case 'CREATE':
await this.create();
break;
case 'EDIT':
await this.update();
break;
default:
break;
}
} catch (error) { } catch (error) {
this.error = error; this.error = error;
console.error(error); console.error(error);
@ -196,8 +217,19 @@ document.addEventListener("alpine:init", async () => {
this.isLoading = false; this.isLoading = false;
} }
}, },
open() { open(event) {
const { labels, actionType, selectedDevice, displayValue } = event.detail
this.isOpen = true; this.isOpen = true;
this.labels = { title: labels.title, submit: labels.submit };
this.actionType = actionType;
this.selectedDevice = selectedDevice;
if (displayValue) {
this.data.id = displayValue.id;
this.data.name = displayValue.name;
this.data.reg_addr = displayValue.reg_addr;
this.data.reg_format = displayValue.reg_format;
this.data.reg_type = displayValue.reg_type;
}
}, },
close() { close() {
this.isOpen = false; this.isOpen = false;

View File

@ -6,13 +6,18 @@ import { validateBodyMiddleware } from "#root/middlewares/index.js";
const modbusRouter = express.Router(); const modbusRouter = express.Router();
modbusRouter.get("/devices", modbusController.list); 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('/create_display-value', validateBodyMiddleware(createDisplayValueSchema), modbusController.createDisplayValue); modbusRouter.post('/create_display-value', validateBodyMiddleware(createDisplayValueSchema), modbusController.createDisplayValue);
modbusRouter.post('/update_display-value', validateBodyMiddleware(displayValueSchema), modbusController.updateDisplayValue)
modbusRouter.post('/remove_display-value', validateBodyMiddleware(removeDisplayValueSchema), modbusController.removeDisplayValue); 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);
modbusRouter.get('/status', modbusController.status); modbusRouter.get('/status', modbusController.status);

View File

@ -10,6 +10,4 @@ export const displayValueSchema = object({
}) })
export const removeDisplayValueSchema = displayValueSchema.pick(['id']); export const removeDisplayValueSchema = displayValueSchema.pick(['id']);
// TODO: "reg_format" can be BOOL if "reg_type" is "DI" or "DO"
export const createDisplayValueSchema = displayValueSchema.omit(['id']) export const createDisplayValueSchema = displayValueSchema.omit(['id'])