This commit is contained in:
Sergey Yarkov 2023-10-24 16:05:33 +03:00
parent fa7c94b908
commit a6173f27e9
No known key found for this signature in database
GPG Key ID: E91CE1FABC619B6F
9 changed files with 167 additions and 113 deletions

BIN
data.db

Binary file not shown.

View File

@ -624,6 +624,10 @@ video {
margin-top: 4rem; margin-top: 4rem;
} }
.mt-2 {
margin-top: 0.5rem;
}
.mt-3 { .mt-3 {
margin-top: 0.75rem; margin-top: 0.75rem;
} }
@ -632,10 +636,6 @@ video {
margin-top: 1rem; margin-top: 1rem;
} }
.mt-2 {
margin-top: 0.5rem;
}
.block { .block {
display: block; display: block;
} }
@ -694,28 +694,28 @@ video {
resize: both; resize: both;
} }
.flex-row {
flex-direction: row;
}
.flex-col { .flex-col {
flex-direction: column; flex-direction: column;
} }
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.space-y-3 > :not([hidden]) ~ :not([hidden]) { .space-y-3 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0; --tw-space-y-reverse: 0;
margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse))); margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(0.75rem * var(--tw-space-y-reverse)); margin-bottom: calc(0.75rem * var(--tw-space-y-reverse));
} }
.space-y-2 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
}
.space-y-10 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(2.5rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(2.5rem * var(--tw-space-y-reverse));
}
.rounded-sm { .rounded-sm {
border-radius: 0.125rem; border-radius: 0.125rem;
} }
@ -761,6 +761,10 @@ video {
text-align: center; text-align: center;
} }
.align-middle {
vertical-align: middle;
}
.text-2xl { .text-2xl {
font-size: 1.5rem; font-size: 1.5rem;
line-height: 2rem; line-height: 2rem;
@ -789,6 +793,15 @@ video {
color: rgb(55 65 81 / var(--tw-text-opacity)); color: rgb(55 65 81 / var(--tw-text-opacity));
} }
.text-red-600 {
--tw-text-opacity: 1;
color: rgb(220 38 38 / var(--tw-text-opacity));
}
.opacity-70 {
opacity: 0.7;
}
.filter { .filter {
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
} }
@ -828,6 +841,11 @@ hr {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
button:disabled {
cursor: not-allowed;
pointer-events: all !important;
}
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
.dark\:text-gray-600 { .dark\:text-gray-600 {
--tw-text-opacity: 1; --tw-text-opacity: 1;

View File

@ -24,3 +24,8 @@ input {
hr { hr {
@apply my-2; @apply my-2;
} }
button:disabled {
cursor: not-allowed;
pointer-events: all !important;
}

View File

@ -26,11 +26,11 @@
Fill in the required configuration settings to connect to the Modbus network. Fill in the required configuration settings to connect to the Modbus network.
</p> </p>
<hr /> <hr />
<div> <!-- <div>
<span>app config state:</span> <span>app config state:</span>
<pre x-text="JSON.stringify($store.app.config, undefined, 2)"></pre> <pre x-text="JSON.stringify($store.app.config, undefined, 2)"></pre>
</div> </div> -->
<form @submit.prevent="configurateApp" action="#"> <form @submit.prevent="configurateApp()" action="#">
<div> <div>
<div> <div>
<label class="block" for="connection_type">Network type</label> <label class="block" for="connection_type">Network type</label>
@ -157,8 +157,20 @@
/> />
</div> </div>
<div class="mt-3"> <div class="mt-3 flex flex-row justify-between items-center">
<button class="btn ml-auto" type="submit">Save</button> <div>
<button
x-bind:disabled="isLoading"
x-bind:class="isLoading && 'opacity-70'"
class="btn ml-auto"
type="submit"
>
Save
</button>
</div>
<div>
<span class="text-red-600" x-text="errorMessage">Error while connection to Modbus network</span>
</div>
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,5 +1,15 @@
const urlPrefix = "/api"; const urlPrefix = "/api";
/**
* Response handler
* @param {Response} response
*/
async function handle(response) {
const data = await response.json();
if (!response.ok) return Promise.reject((data && data.message) || response.statusText);
return data;
}
export default { export default {
async patch(path, data) { async patch(path, data) {
const response = await fetch(`${urlPrefix}${path}`, { const response = await fetch(`${urlPrefix}${path}`, {
@ -9,7 +19,7 @@ export default {
"Content-Type": "application/x-www-form-urlencoded", "Content-Type": "application/x-www-form-urlencoded",
}, },
}); });
return response; return handle(response);
}, },
async post(path, data) { async post(path, data) {
@ -20,6 +30,13 @@ export default {
"Content-Type": "application/x-www-form-urlencoded", "Content-Type": "application/x-www-form-urlencoded",
}, },
}); });
return response; return await handle(response);
},
async get(path) {
const response = await fetch(`${urlPrefix}${path}`, {
method: "GET",
});
return await handle(response);
}, },
}; };

View File

@ -21,28 +21,33 @@ document.addEventListener("alpine:init", async () => {
}, },
currentPage: "loading", currentPage: "loading",
async init() { async init() {
const config = await fetch("/api/app/config"); try {
const is_configured = await fetch("/api/app/is_configured"); const { config } = await api.get("/app/config");
const { is_configured } = await api.get("/app/is_configured");
if (config.ok && is_configured.ok) { this.is_configured = is_configured;
this.config = (await config.json()).config;
this.is_configured = (await is_configured.json()).is_configured;
}
if (this.is_configured) { if (config) this.config = config;
if (is_configured) {
this.currentPage = "monitoring"; this.currentPage = "monitoring";
return; return;
} }
this.currentPage = "configuration"; this.currentPage = "configuration";
} catch (error) {
throw new Error("Error on initializing app.");
}
}, },
}); });
Alpine.data("configApp", () => ({ Alpine.data("configApp", () => ({
selectedConnectionType: "TCP", selectedConnectionType: "TCP",
isLoading: false, isLoading: false,
errorMessage: "",
async configurateApp() { async configurateApp() {
try { this.isLoading = true;
this.errorMessage = "";
let data = {};
const { const {
mb_connection_type, mb_connection_type,
mb_tcp_ip, mb_tcp_ip,
@ -56,34 +61,38 @@ document.addEventListener("alpine:init", async () => {
} = this.$store.app.config; } = this.$store.app.config;
if (mb_connection_type === "TCP") { if (mb_connection_type === "TCP") {
await api.patch("/app/config", { data = {
mb_connection_type, mb_connection_type,
mb_tcp_ip, mb_tcp_ip,
mb_tcp_port, mb_tcp_port,
log_interval_ms, log_interval_ms,
}); };
await api.post("/modbus/connect");
this.$store.app.currentPage = "monitoring";
return;
} }
if (mb_connection_type === "RTU") { if (mb_connection_type === "RTU") {
await api.patch("/app/config", { data = {
mb_connection_type,
mb_rtu_path, mb_rtu_path,
mb_rtu_parity, mb_rtu_parity,
mb_rtu_data_bits, mb_rtu_data_bits,
mb_rtu_stop_bits, mb_rtu_stop_bits,
mb_rtu_baud, mb_rtu_baud,
}); log_interval_ms,
await api.post("/modbus_connect"); };
}
if (data) {
await api.patch("/app/config", data);
try {
await api.post("/modbus/connect");
this.$store.app.currentPage = "monitoring"; this.$store.app.currentPage = "monitoring";
return;
}
} catch (error) { } catch (error) {
console.error("Error while configuring app."); this.errorMessage = "Modbus connection error.";
} }
}
this.isLoading = false;
}, },
})); }));
}); });

View File

@ -1,27 +0,0 @@
import api from "../api";
export async function configurateApp(config) {
const {
mb_connection_type,
mb_tcp_ip,
mb_tcp_port,
mb_rtu_path,
mb_rtu_parity,
mb_rtu_data_bits,
mb_rtu_stop_bits,
mb_rtu_baud,
log_interval_ms,
} = config;
let data = {};
if (config.mb_connection_type === "TCP") {
data = { mb_connection_type, mb_tcp_ip, mb_tcp_port, log_interval_ms };
}
if (config.mb_connection_type === "RTU") {
data = { mb_rtu_path, mb_rtu_parity, mb_rtu_data_bits, mb_rtu_stop_bits, mb_rtu_baud };
}
const configRes = await api.patch("/app/config", data);
if (configRes.ok) await api.post("/modbus/connect");
}

View File

@ -25,7 +25,7 @@ const apiRouter = express.Router();
apiRouter.get("/app/config", async (req, res, next) => { apiRouter.get("/app/config", async (req, res, next) => {
try { try {
const data = await appService.getConfig(db); const data = await appService.getConfig(db);
if (!data) return res.status(404).json({ config: null }); if (!data) return res.status(200).json({ config: null });
return res.status(200).json({ config: data }); return res.status(200).json({ config: data });
} catch (error) { } catch (error) {
next(error); next(error);
@ -52,7 +52,7 @@ apiRouter.patch("/app/config", validateBodyMiddleware(appConfigSchema), async (r
[mb_tcp_ip, mb_tcp_port, payload.log_interval_ms], [mb_tcp_ip, mb_tcp_port, payload.log_interval_ms],
); );
return res.status(200).send("Config Updated."); return res.status(200).json({ message: "Config Updated." });
} }
/* Update RTU configuration */ /* Update RTU configuration */
@ -71,10 +71,10 @@ apiRouter.patch("/app/config", validateBodyMiddleware(appConfigSchema), async (r
`, `,
[mb_rtu_path, mb_rtu_baud, mb_rtu_parity, mb_rtu_data_bits, mb_rtu_stop_bits, payload.log_interval_ms], [mb_rtu_path, mb_rtu_baud, mb_rtu_parity, mb_rtu_data_bits, mb_rtu_stop_bits, payload.log_interval_ms],
); );
return res.status(200).send("Config updated."); return res.status(200).json({ message: "Config updated." });
} }
return res.status(400).send("Undefined Modbus connection type."); return res.status(400).json({ message: "Undefined Modbus connection type." });
} catch (error) { } catch (error) {
next(error); next(error);
} }
@ -88,7 +88,7 @@ apiRouter.get("/app/is_configured", async (req, res, next) => {
const config = await appService.getConfig(db); const config = await appService.getConfig(db);
let is_configured = false; let is_configured = false;
if (!config) return res.status(200).send({ is_configured }); if (!config) return res.status(200).json({ is_configured });
const isValuesExists = (colPrefix) => { const isValuesExists = (colPrefix) => {
const keys = Object.keys(config).filter((k) => k.includes(colPrefix)); const keys = Object.keys(config).filter((k) => k.includes(colPrefix));
@ -99,7 +99,7 @@ apiRouter.get("/app/is_configured", async (req, res, next) => {
if (config.mb_connection_type === "TCP") is_configured = isValuesExists("mb_tcp"); if (config.mb_connection_type === "TCP") is_configured = isValuesExists("mb_tcp");
if (config.mb_connection_type === "RTU") is_configured = isValuesExists("mb_rtu"); if (config.mb_connection_type === "RTU") is_configured = isValuesExists("mb_rtu");
return res.status(200).send({ is_configured }); return res.status(200).json({ is_configured });
} catch (error) { } catch (error) {
next(error); next(error);
} }
@ -111,7 +111,7 @@ apiRouter.get("/app/is_configured", async (req, res, next) => {
apiRouter.post("/modbus/connect", async (req, res, next) => { apiRouter.post("/modbus/connect", async (req, res, next) => {
try { try {
const config = await appService.getConfig(db); const config = await appService.getConfig(db);
if (!config) return res.status(400).send("Application is not configured"); if (!config) return res.status(400).json({ message: "Application is not configured" });
if (modbusClient.isOpen) { if (modbusClient.isOpen) {
modbusClient.close(undefined); modbusClient.close(undefined);
@ -120,7 +120,7 @@ apiRouter.post("/modbus/connect", async (req, res, next) => {
if (config.mb_connection_type === "TCP") { if (config.mb_connection_type === "TCP") {
await modbusClient.connectTCP(config.mb_tcp_ip, { port: config.mb_tcp_port }); await modbusClient.connectTCP(config.mb_tcp_ip, { port: config.mb_tcp_port });
return res.status(200).send("Connected."); return res.status(200).json({ message: "Connected." });
} }
if (config.mb_connection_type === "RTU") { if (config.mb_connection_type === "RTU") {
@ -130,10 +130,10 @@ apiRouter.post("/modbus/connect", async (req, res, next) => {
parity: config.mb_rtu_parity, parity: config.mb_rtu_parity,
stopBits: config.mb_rtu_stop_bits, stopBits: config.mb_rtu_stop_bits,
}); });
return res.status(200).send("Connected."); return res.status(200).json({ message: "Connected." });
} }
return res.status(400).send("Connection type is invalid."); return res.status(400).json({ message: "Connection type is invalid." });
} catch (error) { } catch (error) {
next(error); next(error);
} }
@ -141,7 +141,7 @@ apiRouter.post("/modbus/connect", async (req, res, next) => {
apiRouter.post("/modbus/close", (req, res, next) => { apiRouter.post("/modbus/close", (req, res, next) => {
try { try {
if (!modbusClient.isOpen) return res.status(400).send("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);
} catch (error) { } catch (error) {
@ -149,4 +149,24 @@ apiRouter.post("/modbus/close", (req, res, next) => {
} }
}); });
apiRouter.get("/modbus/data_stream", async (req, res, next) => {
try {
if (!modbusClient.isOpen) return res.status(503).json({ message: "Modbus connection closed." });
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Connection", "keep-alive");
let interval = setInterval(async () => {
const { data } = await modbusClient.readHoldingRegisters(0, 1);
res.write("event: messsage\n");
res.write(`data: ${JSON.stringify({ data })}`);
res.write("\n\n");
}, 1000);
req.on("close", () => clearInterval(interval));
} catch (error) {
next(error);
}
});
export { apiRouter }; export { apiRouter };

View File

@ -1,13 +1,13 @@
CREATE TABLE IF NOT EXISTS "app_config" ( CREATE TABLE IF NOT EXISTS "app_config" (
"id" INTEGER, "id" INTEGER,
"mb_connection_type" VARCHAR(3) NOT NULL DEFAULT "RTU" CHECK(mb_connection_type IN ('TCP','RTU')), "mb_connection_type" VARCHAR(3) NOT NULL DEFAULT "RTU" CHECK(mb_connection_type IN ('TCP','RTU')),
"mb_tcp_ip" VARCHAR(15), "mb_tcp_ip" VARCHAR(15) NOT NULL DEFAULT "127.0.0.1",
"mb_tcp_port" INTEGER, "mb_tcp_port" INTEGER NOT NULL DEFAULT 502,
"mb_rtu_path" VARCHAR(10), "mb_rtu_path" VARCHAR(10) NOT NULL DEFAULT "",
"mb_rtu_baud" REAL, "mb_rtu_baud" INTEGER NOT NULL DEFAULT 9600,
"mb_rtu_parity" VARCHAR(5) CHECK(mb_rtu_parity IN ("even", "odd", "none")), "mb_rtu_parity" VARCHAR(5) NOT NULL DEFAULT "none" CHECK(mb_rtu_parity IN ("even", "odd", "none")),
"mb_rtu_data_bits" INTEGER, "mb_rtu_data_bits" INTEGER NOT NULL DEFAULT 8,
"mb_rtu_stop_bits" INTEGER, "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 >= 1000)