diff --git a/data.db b/data.db index b695e92..194889d 100644 Binary files a/data.db and b/data.db differ diff --git a/public/css/dist.css b/public/css/dist.css index 71deb23..835d735 100644 --- a/public/css/dist.css +++ b/public/css/dist.css @@ -624,6 +624,10 @@ video { margin-top: 4rem; } +.mt-2 { + margin-top: 0.5rem; +} + .mt-3 { margin-top: 0.75rem; } @@ -632,10 +636,6 @@ video { margin-top: 1rem; } -.mt-2 { - margin-top: 0.5rem; -} - .block { display: block; } @@ -694,28 +694,28 @@ video { resize: both; } +.flex-row { + flex-direction: row; +} + .flex-col { flex-direction: column; } +.items-center { + align-items: center; +} + +.justify-between { + justify-content: space-between; +} + .space-y-3 > :not([hidden]) ~ :not([hidden]) { --tw-space-y-reverse: 0; margin-top: calc(0.75rem * calc(1 - 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 { border-radius: 0.125rem; } @@ -761,6 +761,10 @@ video { text-align: center; } +.align-middle { + vertical-align: middle; +} + .text-2xl { font-size: 1.5rem; line-height: 2rem; @@ -789,6 +793,15 @@ video { 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: 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; } +button:disabled { + cursor: not-allowed; + pointer-events: all !important; +} + @media (prefers-color-scheme: dark) { .dark\:text-gray-600 { --tw-text-opacity: 1; diff --git a/public/css/main.css b/public/css/main.css index 769ee92..ca2daa1 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -24,3 +24,8 @@ input { hr { @apply my-2; } + +button:disabled { + cursor: not-allowed; + pointer-events: all !important; +} diff --git a/public/index.html b/public/index.html index 23f05d1..2021b64 100644 --- a/public/index.html +++ b/public/index.html @@ -26,11 +26,11 @@ Fill in the required configuration settings to connect to the Modbus network.


-
+ +
@@ -157,8 +157,20 @@ />
-
- +
+
+ +
+
+ Error while connection to Modbus network +
diff --git a/public/js/api.js b/public/js/api.js index 82caf60..ea1ef7a 100644 --- a/public/js/api.js +++ b/public/js/api.js @@ -1,5 +1,15 @@ 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 { async patch(path, data) { const response = await fetch(`${urlPrefix}${path}`, { @@ -9,7 +19,7 @@ export default { "Content-Type": "application/x-www-form-urlencoded", }, }); - return response; + return handle(response); }, async post(path, data) { @@ -20,6 +30,13 @@ export default { "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); }, }; diff --git a/public/js/app.js b/public/js/app.js index 3f6b371..6523735 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -21,69 +21,78 @@ document.addEventListener("alpine:init", async () => { }, currentPage: "loading", async init() { - const config = await fetch("/api/app/config"); - const is_configured = await fetch("/api/app/is_configured"); + try { + const { config } = await api.get("/app/config"); + const { is_configured } = await api.get("/app/is_configured"); - if (config.ok && is_configured.ok) { - this.config = (await config.json()).config; - this.is_configured = (await is_configured.json()).is_configured; + this.is_configured = is_configured; + + if (config) this.config = config; + if (is_configured) { + this.currentPage = "monitoring"; + return; + } + + this.currentPage = "configuration"; + } catch (error) { + throw new Error("Error on initializing app."); } - - if (this.is_configured) { - this.currentPage = "monitoring"; - return; - } - - this.currentPage = "configuration"; }, }); Alpine.data("configApp", () => ({ selectedConnectionType: "TCP", isLoading: false, + errorMessage: "", async configurateApp() { - try { - const { + this.isLoading = true; + this.errorMessage = ""; + let data = {}; + const { + mb_connection_type, + mb_tcp_ip, + mb_tcp_port, + mb_rtu_path, + mb_rtu_baud, + mb_rtu_parity, + mb_rtu_data_bits, + mb_rtu_stop_bits, + log_interval_ms, + } = this.$store.app.config; + + if (mb_connection_type === "TCP") { + data = { mb_connection_type, mb_tcp_ip, mb_tcp_port, + log_interval_ms, + }; + } + + if (mb_connection_type === "RTU") { + data = { + mb_connection_type, mb_rtu_path, - mb_rtu_baud, mb_rtu_parity, mb_rtu_data_bits, mb_rtu_stop_bits, + mb_rtu_baud, log_interval_ms, - } = this.$store.app.config; - - if (mb_connection_type === "TCP") { - await api.patch("/app/config", { - mb_connection_type, - mb_tcp_ip, - mb_tcp_port, - log_interval_ms, - }); - - await api.post("/modbus/connect"); - - this.$store.app.currentPage = "monitoring"; - return; - } - - if (mb_connection_type === "RTU") { - await api.patch("/app/config", { - mb_rtu_path, - mb_rtu_parity, - mb_rtu_data_bits, - mb_rtu_stop_bits, - mb_rtu_baud, - }); - await api.post("/modbus_connect"); - this.$store.app.currentPage = "monitoring"; - return; - } - } catch (error) { - console.error("Error while configuring app."); + }; } + + if (data) { + await api.patch("/app/config", data); + + try { + await api.post("/modbus/connect"); + this.$store.app.currentPage = "monitoring"; + } catch (error) { + this.errorMessage = "Modbus connection error."; + } + } + + this.isLoading = false; }, })); }); diff --git a/public/js/services/app.service.js b/public/js/services/app.service.js deleted file mode 100644 index 912f6f2..0000000 --- a/public/js/services/app.service.js +++ /dev/null @@ -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"); -} diff --git a/routes/api.router.js b/routes/api.router.js index 6d8f088..4eb4bf4 100644 --- a/routes/api.router.js +++ b/routes/api.router.js @@ -25,7 +25,7 @@ const apiRouter = express.Router(); apiRouter.get("/app/config", async (req, res, next) => { try { 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 }); } catch (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], ); - return res.status(200).send("Config Updated."); + return res.status(200).json({ message: "Config Updated." }); } /* 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], ); - 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) { next(error); } @@ -88,7 +88,7 @@ apiRouter.get("/app/is_configured", async (req, res, next) => { const config = await appService.getConfig(db); 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 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 === "RTU") is_configured = isValuesExists("mb_rtu"); - return res.status(200).send({ is_configured }); + return res.status(200).json({ is_configured }); } catch (error) { next(error); } @@ -111,7 +111,7 @@ apiRouter.get("/app/is_configured", async (req, res, next) => { apiRouter.post("/modbus/connect", async (req, res, next) => { try { 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) { modbusClient.close(undefined); @@ -120,7 +120,7 @@ apiRouter.post("/modbus/connect", async (req, res, next) => { if (config.mb_connection_type === "TCP") { 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") { @@ -130,10 +130,10 @@ apiRouter.post("/modbus/connect", async (req, res, next) => { parity: config.mb_rtu_parity, 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) { next(error); } @@ -141,7 +141,7 @@ apiRouter.post("/modbus/connect", async (req, res, next) => { apiRouter.post("/modbus/close", (req, res, next) => { 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.destroy(undefined); } 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 }; diff --git a/sql/init-schema.sql b/sql/init-schema.sql index 5cce2fb..fb62ccc 100644 --- a/sql/init-schema.sql +++ b/sql/init-schema.sql @@ -1,13 +1,13 @@ CREATE TABLE IF NOT EXISTS "app_config" ( "id" INTEGER, "mb_connection_type" VARCHAR(3) NOT NULL DEFAULT "RTU" CHECK(mb_connection_type IN ('TCP','RTU')), - "mb_tcp_ip" VARCHAR(15), - "mb_tcp_port" INTEGER, - "mb_rtu_path" VARCHAR(10), - "mb_rtu_baud" REAL, - "mb_rtu_parity" VARCHAR(5) CHECK(mb_rtu_parity IN ("even", "odd", "none")), - "mb_rtu_data_bits" INTEGER, - "mb_rtu_stop_bits" INTEGER, + "mb_tcp_ip" VARCHAR(15) NOT NULL DEFAULT "127.0.0.1", + "mb_tcp_port" INTEGER NOT NULL DEFAULT 502, + "mb_rtu_path" VARCHAR(10) NOT NULL DEFAULT "", + "mb_rtu_baud" INTEGER NOT NULL DEFAULT 9600, + "mb_rtu_parity" VARCHAR(5) NOT NULL DEFAULT "none" CHECK(mb_rtu_parity IN ("even", "odd", "none")), + "mb_rtu_data_bits" INTEGER NOT NULL DEFAULT 8, + "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)