This commit is contained in:
Sergey Yarkov 2023-11-07 16:25:59 +03:00
parent 0252681847
commit 582cbd285e
11 changed files with 155 additions and 47 deletions

View File

@ -1,5 +1,6 @@
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 { appService, modbusService } from "#root/services/index.js"; import { appService, modbusService } from "#root/services/index.js";
export const modbusController = { export const modbusController = {
@ -70,7 +71,7 @@ export const modbusController = {
"g_display_reg_format", "g_display_reg_format",
"g_display_reg_type", "g_display_reg_type",
"g_y_label") "g_y_label")
VALUES (?, ?, ?, ?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?, ?)`,
[id, name, g_display_reg_addr, g_display_reg_format, g_display_reg_type, g_y_label], [id, name, g_display_reg_addr, g_display_reg_format, g_display_reg_type, g_y_label],
); );
return res.status(200).json({ message: "Device created." }); return res.status(200).json({ message: "Device created." });
@ -80,13 +81,47 @@ export const modbusController = {
}, },
/** /**
* Remove modbus slave device * Update modbus slave device
* @param {import('express').Request} req * @param {import('express').Request} req
* @param {import('express').Response} res * @param {import('express').Response} res
* @param {import('express').NextFunction} next * @param {import('express').NextFunction} next
*/ */
async updateDevice(req, res, next) { async updateDevice(req, res, next) {
try { try {
const {
id,
newId,
name,
g_display_reg_addr,
g_display_reg_type,
g_display_reg_format,
g_y_label
} = req.body;
await db.run(`UPDATE "modbus_slaves" SET
id = ?,
name = ?,
g_display_reg_addr = NULL,
g_display_reg_type = NULL,
g_display_reg_format = NULL,
g_y_label = NULL
WHERE id = ? `,
[newId, name, id]);
if (g_display_reg_addr !== null &&
g_display_reg_type !== null &&
g_display_reg_format !== null &&
g_y_label !== null
) {
await db.run(`UPDATE "modbus_slaves" SET
g_display_reg_addr = ?,
g_display_reg_type = ?,
g_display_reg_format = ?,
g_y_label = ? WHERE id = ?`,
[g_display_reg_addr, g_display_reg_type, g_display_reg_format, g_y_label, id]);
}
return res.status(200).json({ message: 'Updated.' })
} catch (error) { } catch (error) {
next(error); next(error);
} }
@ -152,12 +187,8 @@ export const modbusController = {
*/ */
async status(req, res, next) { async status(req, res, next) {
try { try {
res.setHeader("Cache-Control", "no-cache"); utils.setSSEHeaders(res);
res.setHeader("Content-Type", "text/event-stream"); const intervalId = modbusService.startConnectionStatusInterval((status) => res.write(utils.serializeSSEData(status, 'message')))
res.setHeader("Connection", "keep-alive");
let intervalId = setInterval(() => {
res.write(`event: message\ndata: ${JSON.stringify(modbusClient.isOpen)}\n\n`);
}, 500);
req.on("close", () => clearInterval(intervalId)); req.on("close", () => clearInterval(intervalId));
} catch (error) { } catch (error) {
next(error); next(error);
@ -180,38 +211,20 @@ export const modbusController = {
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) return res.status(404).json({ error: { message: "Device not found." } });
res.setHeader("Cache-Control", "no-cache"); utils.setSSEHeaders(res);
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Connection", "keep-alive");
const intervalId = modbusService.startDevicePollInterval( const intervalId = await modbusService.startDevicePollInterval(
device, device,
(data, error) => { (data, error) => {
if (error) { if (error) {
next(error); next(error);
return; return;
} }
res.write("event: message\n"); res.write(utils.serializeSSEData(data, 'message'));
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, },
1000,
); );
req.on("close", () => clearInterval(intervalId)); req.on("close", () => clearInterval(intervalId));
// const streamInterval = setInterval(async () => {
// try {
// device.display_values = await db.all(`SELECT * FROM "display_values" WHERE slave_id = ?`, [slave_id])
// const data = await modbusService.readDataFromDevice(device);
// res.write("event: message\n");
// res.write(`data: ${JSON.stringify(data)}\n\n`);
// } catch (error) {
// clearInterval(streamInterval);
// next(error);
// }
// }, 1000);
// req.on("close", () => clearInterval(streamInterval));
} catch (error) { } catch (error) {
next(error); next(error);
} }

BIN
data.db

Binary file not shown.

View File

@ -14,7 +14,7 @@ CREATE TABLE IF NOT EXISTS "app_config" (
"mb_rtu_stop_bits" INTEGER NOT NULL DEFAULT 1, "mb_rtu_stop_bits" INTEGER NOT NULL DEFAULT 1,
"log_interval_ms" INTEGER DEFAULT 1000 NOT NULL, "log_interval_ms" INTEGER DEFAULT 1000 NOT NULL,
CONSTRAINT "id_unique" CHECK(id=0), CONSTRAINT "id_unique" CHECK(id=0),
CONSTRAINT "min_interval_check" CHECK(log_interval_ms >= 1000) CONSTRAINT "min_interval_check" CHECK(log_interval_ms >= 500)
PRIMARY KEY("id") PRIMARY KEY("id")
); );
@ -36,7 +36,7 @@ CREATE TABLE IF NOT EXISTS "display_values" (
"reg_type" VARCHAR(3) NOT NULL CHECK(reg_type IN("HR", "IR", "DI", "DO")) DEFAULT "IR", "reg_type" VARCHAR(3) NOT NULL CHECK(reg_type IN("HR", "IR", "DI", "DO")) DEFAULT "IR",
"reg_format" VARCHAR(5) NOT NULL CHECK(reg_format IN("UI16", "I16", "UI32", "I32", "FP32", "BOOL")) DEFAULT "UI16", "reg_format" VARCHAR(5) NOT NULL CHECK(reg_format IN("UI16", "I16", "UI32", "I32", "FP32", "BOOL")) DEFAULT "UI16",
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 ON UPDATE CASCADE
); );
-- CREATE TABLE IF NOT EXISTS "modbus_log" ( -- CREATE TABLE IF NOT EXISTS "modbus_log" (

View File

@ -46,14 +46,14 @@
<template x-teleport="body"> <template x-teleport="body">
<div <div
x-data="deviceModal" x-data="deviceModal"
@open-create-device-modal.window="open()" @open-device-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 device</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
@ -64,7 +64,7 @@
</button> </button>
</div> </div>
<hr /> <hr />
<form @submit.prevent="create(() => graphView = 'none')" action="#"> <form @submit.prevent="submit" action="#">
<div <div
x-id="['id', 'name', 'graph-display', 'g_display_reg_addr', 'g_display_reg_format', 'g_y_label']" x-id="['id', 'name', 'graph-display', 'g_display_reg_addr', 'g_display_reg_format', 'g_y_label']"
class="flex flex-col space-y-2" class="flex flex-col space-y-2"
@ -167,12 +167,12 @@
<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 x-text="labels.cancel" @click="close()" class="btn bg-red-500"></button>
</div> </div>
</form> </form>
</div> </div>
@ -180,7 +180,10 @@
</template> </template>
<!-- Create new device --> <!-- Create new device -->
<button <button
@click="$dispatch('open-create-device-modal')" @click="$dispatch('open-device-modal', {
labels: { title: 'Create new device', submit: 'Create', cancel: 'Cancel' },
actionType: 'CREATE'
})"
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>
@ -209,7 +212,13 @@
</div> </div>
<!-- Device create, delete buttons --> <!-- Device create, delete buttons -->
<div> <div>
<a href="#">Edit</a> <a
@click="$dispatch('open-device-modal', {
labels: { title: `Edit device ${selectedDevice?.name}`, submit: 'Save', cancel: 'Cancel' },
device: selectedDevice,
actionType: 'EDIT'
})"
href="#">Edit</a>
<a <a
@click="$dispatch('dialog', { @click="$dispatch('dialog', {
title: 'Remove device', title: 'Remove device',
@ -569,7 +578,8 @@
id="log_interval_ms" id="log_interval_ms"
name="log_interval_ms" name="log_interval_ms"
type="number" type="number"
min="1000" min="500"
max="10000"
x-model.number="$store.app.config.log_interval_ms" x-model.number="$store.app.config.log_interval_ms"
required required
/> />

View File

@ -64,13 +64,20 @@ document.addEventListener("alpine:init", async () => {
error: null, error: null,
graphView: "", graphView: "",
data: { data: {
oldId: null,
id: null, id: null,
name: "", name: "",
g_display_reg_addr: null, g_display_reg_addr: 0,
g_display_reg_format: "UI16", g_display_reg_format: "UI16",
g_display_reg_type: "IR", g_display_reg_type: "IR",
g_y_label: "", g_y_label: "",
}, },
labels: {
title: '',
submit: '',
cancel: ''
},
actionType: 'CREATE',
resetDataFields() { resetDataFields() {
this.data.id = null; this.data.id = null;
this.data.name = ""; this.data.name = "";
@ -95,7 +102,57 @@ document.addEventListener("alpine:init", async () => {
this.isLoading = false; this.isLoading = false;
} }
}, },
open() { async edit() {
try {
this.isLoading = true;
this.error = null;
if (this.graphView === 'none') {
this.data.g_display_reg_addr = null;
this.data.g_display_reg_format = null;
this.data.g_display_reg_type = null;
this.data.g_y_label = null;
}
await api.post('/modbus/update_device', utils.dellNullableKeys({ ...this.data, id: this.data.oldId, newId: this.data.id }));
window.location.reload();
} catch (error) {
this.error = error;
console.error(error);
} finally {
this.isLoading = false;
}
},
async submit() {
switch (this.actionType) {
case 'CREATE':
await this.create()
this.graphView = 'none'
break;
case 'EDIT':
await this.edit()
break;
default:
break;
}
},
open(event) {
const { detail } = event;
this.labels = {
title: detail.labels.title,
body: detail.labels.body,
submit: detail.labels.submit,
cancel: detail.labels.cancel
};
this.actionType = detail.actionType;
if (detail.device) {
this.data.oldId = detail.device.id;
this.data.id = detail.device.id;
this.data.name = detail.device.name;
this.data.g_display_reg_addr = detail.device.g_display_reg_addr || 0;
this.data.g_display_reg_format = detail.device.g_display_reg_format || 'UI16';
this.data.g_display_reg_type = detail.device.g_display_reg_type || 'IR';
this.data.g_y_label = detail.device.g_y_label || '';
if (detail.device.g_display_reg_addr !== null) this.graphView = 'new-value'
}
this.isOpen = true; this.isOpen = true;
}, },
close() { close() {

View File

@ -147,9 +147,9 @@ export const modbusService = {
/** /**
* @param {import("..").ModbusDevice} device * @param {import("..").ModbusDevice} device
* @param {(data: import("..").EventDataStream, error: any) => void} callback * @param {(data: import("..").EventDataStream, error: any) => void} callback
* @param {number} intervalMs
*/ */
startDevicePollInterval(device, callback, intervalMs) { async startDevicePollInterval(device, callback) {
const { log_interval_ms: intervalMs } = await db.get(`SELECT "log_interval_ms" FROM "app_config"`);
const poll = async () => { const poll = async () => {
try { try {
device.display_values = await db.all(`SELECT * FROM "display_values" WHERE slave_id = ?`, [device.id]); device.display_values = await db.all(`SELECT * FROM "display_values" WHERE slave_id = ?`, [device.id]);
@ -161,4 +161,11 @@ export const modbusService = {
}; };
return setInterval(poll, intervalMs); return setInterval(poll, intervalMs);
}, },
/**
* @param {(isOpen: boolean) => void} callback
*/
startConnectionStatusInterval(callback) {
return setInterval(() => callback(modbusClient.isOpen), 500)
}
}; };

17
utils/http.js Normal file
View File

@ -0,0 +1,17 @@
/**
* @param {import('express').Response} res
*/
export function setSSEHeaders(res) {
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Connection", "keep-alive");
}
/**
* @param {any} data
* @param {string} eventType
*/
export function serializeSSEData(data, eventType) {
return `event: ${eventType}\ndata: ${JSON.stringify(data)}\n\n`;
}

View File

@ -1 +1,2 @@
export * from './number.js' export * from './number.js'
export * from './http.js'

View File

@ -11,5 +11,5 @@ export const displayValueSchema = object({
export const removeDisplayValueSchema = displayValueSchema.pick(['id']); export const removeDisplayValueSchema = displayValueSchema.pick(['id']);
// TODO: On creating "reg_format" can be BOOL if "reg_type" is "DI" or "DO" // 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'])

View File

@ -14,7 +14,7 @@ export const appConfigSchema = object({
mb_rtu_parity: string().oneOf(["none", "even", "odd"]).when("mb_connection_type", requiredIfConnTypeEquals("RTU")), mb_rtu_parity: string().oneOf(["none", "even", "odd"]).when("mb_connection_type", requiredIfConnTypeEquals("RTU")),
mb_rtu_data_bits: number().oneOf([7, 8, 9]).when("mb_connection_type", requiredIfConnTypeEquals("RTU")), mb_rtu_data_bits: number().oneOf([7, 8, 9]).when("mb_connection_type", requiredIfConnTypeEquals("RTU")),
mb_rtu_stop_bits: number().oneOf([1, 2]).when("mb_connection_type", requiredIfConnTypeEquals("RTU")), mb_rtu_stop_bits: number().oneOf([1, 2]).when("mb_connection_type", requiredIfConnTypeEquals("RTU")),
log_interval_ms: number().min(1000).required(), log_interval_ms: number().min(500).max(10000).required(),
}); });
export * from './modbus-device.schema.js' export * from './modbus-device.schema.js'

View File

@ -9,4 +9,7 @@ export const modbusDeviceSchema = object({
g_y_label: string().max(40).nullable(), g_y_label: string().max(40).nullable(),
}); });
export const removeModbusDeviceSchema = modbusDeviceSchema.pick(['id']); export const removeModbusDeviceSchema = modbusDeviceSchema.pick(['id']);
export const editModbusDeviceSchema = modbusDeviceSchema.shape({
newId: number().required()
})