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
0252681847
commit
582cbd285e
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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" (
|
||||||
|
|||||||
@ -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
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
@ -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
17
utils/http.js
Normal 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`;
|
||||||
|
}
|
||||||
@ -1 +1,2 @@
|
|||||||
export * from './number.js'
|
export * from './number.js'
|
||||||
|
export * from './http.js'
|
||||||
@ -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'])
|
||||||
@ -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'
|
||||||
|
|||||||
@ -10,3 +10,6 @@ export const modbusDeviceSchema = object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const removeModbusDeviceSchema = modbusDeviceSchema.pick(['id']);
|
export const removeModbusDeviceSchema = modbusDeviceSchema.pick(['id']);
|
||||||
|
export const editModbusDeviceSchema = modbusDeviceSchema.shape({
|
||||||
|
newId: number().required()
|
||||||
|
})
|
||||||
Loading…
x
Reference in New Issue
Block a user