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 modbusClient from "#root/config/modbus-client.config.js";
import * as utils from '#root/utils/index.js'
import { appService, modbusService } from "#root/services/index.js";
export const modbusController = {
@ -70,7 +71,7 @@ export const modbusController = {
"g_display_reg_format",
"g_display_reg_type",
"g_y_label")
VALUES (?, ?, ?, ?, ?, ?, ?)`,
VALUES (?, ?, ?, ?, ?, ?)`,
[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." });
@ -80,13 +81,47 @@ export const modbusController = {
},
/**
* Remove modbus slave device
* Update modbus slave device
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {import('express').NextFunction} next
*/
async updateDevice(req, res, next) {
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) {
next(error);
}
@ -152,12 +187,8 @@ export const modbusController = {
*/
async status(req, res, next) {
try {
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Connection", "keep-alive");
let intervalId = setInterval(() => {
res.write(`event: message\ndata: ${JSON.stringify(modbusClient.isOpen)}\n\n`);
}, 500);
utils.setSSEHeaders(res);
const intervalId = modbusService.startConnectionStatusInterval((status) => res.write(utils.serializeSSEData(status, 'message')))
req.on("close", () => clearInterval(intervalId));
} catch (error) {
next(error);
@ -180,38 +211,20 @@ export const modbusController = {
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." } });
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Connection", "keep-alive");
utils.setSSEHeaders(res);
const intervalId = modbusService.startDevicePollInterval(
const intervalId = await modbusService.startDevicePollInterval(
device,
(data, error) => {
if (error) {
next(error);
return;
}
res.write("event: message\n");
res.write(`data: ${JSON.stringify(data)}\n\n`);
res.write(utils.serializeSSEData(data, 'message'));
},
1000,
);
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) {
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,
"log_interval_ms" INTEGER DEFAULT 1000 NOT NULL,
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")
);
@ -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_format" VARCHAR(5) NOT NULL CHECK(reg_format IN("UI16", "I16", "UI32", "I32", "FP32", "BOOL")) DEFAULT "UI16",
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" (

View File

@ -46,14 +46,14 @@
<template x-teleport="body">
<div
x-data="deviceModal"
@open-create-device-modal.window="open()"
@open-device-modal.window="open($event)"
@keyup.escape.window="close()"
class="backdrop z-50"
x-show="isOpen"
>
<div class="modal">
<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">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path
@ -64,7 +64,7 @@
</button>
</div>
<hr />
<form @submit.prevent="create(() => graphView = 'none')" action="#">
<form @submit.prevent="submit" action="#">
<div
x-id="['id', 'name', 'graph-display', 'g_display_reg_addr', 'g_display_reg_format', 'g_y_label']"
class="flex flex-col space-y-2"
@ -167,12 +167,12 @@
<button
x-bind:disabled="isLoading"
x-bind:class="isLoading && 'opacity-70'"
x-text="labels.submit"
type="submit"
class="btn"
>
Create
</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>
</form>
</div>
@ -180,7 +180,10 @@
</template>
<!-- Create new device -->
<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"
>
<span class="text-4xl text-gray-400 select-none">+</span>
@ -209,7 +212,13 @@
</div>
<!-- Device create, delete buttons -->
<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
@click="$dispatch('dialog', {
title: 'Remove device',
@ -569,7 +578,8 @@
id="log_interval_ms"
name="log_interval_ms"
type="number"
min="1000"
min="500"
max="10000"
x-model.number="$store.app.config.log_interval_ms"
required
/>

View File

@ -64,13 +64,20 @@ document.addEventListener("alpine:init", async () => {
error: null,
graphView: "",
data: {
oldId: null,
id: null,
name: "",
g_display_reg_addr: null,
g_display_reg_addr: 0,
g_display_reg_format: "UI16",
g_display_reg_type: "IR",
g_y_label: "",
},
labels: {
title: '',
submit: '',
cancel: ''
},
actionType: 'CREATE',
resetDataFields() {
this.data.id = null;
this.data.name = "";
@ -95,7 +102,57 @@ document.addEventListener("alpine:init", async () => {
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;
},
close() {

View File

@ -147,9 +147,9 @@ export const modbusService = {
/**
* @param {import("..").ModbusDevice} device
* @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 () => {
try {
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);
},
/**
* @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 './http.js'

View File

@ -11,5 +11,5 @@ export const displayValueSchema = object({
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'])

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_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")),
log_interval_ms: number().min(1000).required(),
log_interval_ms: number().min(500).max(10000).required(),
});
export * from './modbus-device.schema.js'

View File

@ -10,3 +10,6 @@ export const modbusDeviceSchema = object({
});
export const removeModbusDeviceSchema = modbusDeviceSchema.pick(['id']);
export const editModbusDeviceSchema = modbusDeviceSchema.shape({
newId: number().required()
})