mirror of
https://github.com/sergeyyarkov/modbus-logger.git
synced 2026-10-11 18:38:51 +03:00
setup
This commit is contained in:
commit
e3e031ea1d
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
.env
|
||||
6
.prettierrc
Normal file
6
.prettierrc
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"printWidth": 120,
|
||||
"semi": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
16
app/index.js
Normal file
16
app/index.js
Normal file
@ -0,0 +1,16 @@
|
||||
import express from "express";
|
||||
import pino from "pino-http";
|
||||
|
||||
/**
|
||||
* Routers
|
||||
*/
|
||||
import { apiRouter } from "../routes/api.router.js";
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(pino());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.static("public"));
|
||||
app.use("/api", apiRouter);
|
||||
|
||||
export default app;
|
||||
9
config/database.config.js
Normal file
9
config/database.config.js
Normal file
@ -0,0 +1,9 @@
|
||||
// @ts-nocheck
|
||||
import sqlite3 from "sqlite3";
|
||||
import { open } from "sqlite";
|
||||
|
||||
const sqlite = process.env.NODE_ENV == "development" ? sqlite3.verbose() : sqlite3;
|
||||
|
||||
const db = await open({ filename: "./data.db", driver: sqlite.Database });
|
||||
|
||||
export default db;
|
||||
5
config/env.config.js
Normal file
5
config/env.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
import 'dotenv/config';
|
||||
|
||||
export default {
|
||||
SERVER_PORT: Number.parseInt(process.env?.SERVER_PORT, 10) || 3000,
|
||||
};
|
||||
7
config/modbus-client.config.js
Normal file
7
config/modbus-client.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
import Modbus from 'modbus-serial';
|
||||
|
||||
const modbusClient = new Modbus();
|
||||
|
||||
modbusClient.on('close', () => console.log('Modbus connection closed.'));
|
||||
|
||||
export default modbusClient;
|
||||
11
index.d.ts
vendored
Normal file
11
index.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
export type AppConfig = {
|
||||
mb_connection_type: "TCP" | "RTU";
|
||||
mb_tcp_ip: string | null;
|
||||
mb_tcp_port: number | null;
|
||||
mb_rtu_path: string | null;
|
||||
mb_rtu_baud: number | null;
|
||||
mb_rtu_parity: "even" | "odd" | "none" | null;
|
||||
mb_rtu_data_bits: number | null;
|
||||
mb_rtu_stop_bits: number | null;
|
||||
log_interval_ms: number;
|
||||
};
|
||||
9
jsconfig.json
Normal file
9
jsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"#root/*": ["./*"]
|
||||
},
|
||||
"checkJs": true
|
||||
}
|
||||
}
|
||||
1
middlewares/index.js
Normal file
1
middlewares/index.js
Normal file
@ -0,0 +1 @@
|
||||
export * from "./validate-body.middleware.js";
|
||||
19
middlewares/validate-body.middleware.js
Normal file
19
middlewares/validate-body.middleware.js
Normal file
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Validate request body
|
||||
* @param {import('yup').Schema} schema
|
||||
*/
|
||||
export function validateBodyMiddleware(schema) {
|
||||
/**
|
||||
* @param {import('express').Request} req
|
||||
* @param {import('express').Response} res
|
||||
* @param {import('express').NextFunction} next
|
||||
*/
|
||||
return async (req, res, next) => {
|
||||
try {
|
||||
await schema.validate(req.body);
|
||||
next();
|
||||
} catch (error) {
|
||||
return res.status(400).send(error.message);
|
||||
}
|
||||
};
|
||||
}
|
||||
4
nodemon.json
Normal file
4
nodemon.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"watch": [".env", "*/*.js"],
|
||||
"ignore": ["./public"]
|
||||
}
|
||||
2549
package-lock.json
generated
Normal file
2549
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
package.json
Normal file
31
package.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "modbus-logger-app",
|
||||
"version": "0.0.1",
|
||||
"description": "Monitoring Modbus slave devices",
|
||||
"main": "app.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js",
|
||||
"db:init": "node ./scripts/db-init.script.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Sergey Yarkov",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"modbus-serial": "^8.0.13",
|
||||
"pino-http": "^8.5.0",
|
||||
"sqlite": "^5.0.1",
|
||||
"sqlite3": "^5.1.6",
|
||||
"yup": "^1.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1",
|
||||
"tailwindcss": "^3.3.3"
|
||||
},
|
||||
"imports": {
|
||||
"#root/*": "./*"
|
||||
}
|
||||
}
|
||||
870
public/css/dist.css
Normal file
870
public/css/dist.css
Normal file
@ -0,0 +1,870 @@
|
||||
/*
|
||||
! tailwindcss v3.3.3 | MIT License | https://tailwindcss.com
|
||||
*/
|
||||
|
||||
/*
|
||||
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
|
||||
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
|
||||
*/
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
/* 1 */
|
||||
border-width: 0;
|
||||
/* 2 */
|
||||
border-style: solid;
|
||||
/* 2 */
|
||||
border-color: #e5e7eb;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
::before,
|
||||
::after {
|
||||
--tw-content: '';
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use a consistent sensible line-height in all browsers.
|
||||
2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
3. Use a more readable tab size.
|
||||
4. Use the user's configured `sans` font-family by default.
|
||||
5. Use the user's configured `sans` font-feature-settings by default.
|
||||
6. Use the user's configured `sans` font-variation-settings by default.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.5;
|
||||
/* 1 */
|
||||
-webkit-text-size-adjust: 100%;
|
||||
/* 2 */
|
||||
-moz-tab-size: 4;
|
||||
/* 3 */
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
/* 3 */
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
/* 4 */
|
||||
font-feature-settings: normal;
|
||||
/* 5 */
|
||||
font-variation-settings: normal;
|
||||
/* 6 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove the margin in all browsers.
|
||||
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
/* 1 */
|
||||
line-height: inherit;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Add the correct height in Firefox.
|
||||
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
|
||||
3. Ensure horizontal rules are visible by default.
|
||||
*/
|
||||
|
||||
hr {
|
||||
height: 0;
|
||||
/* 1 */
|
||||
color: inherit;
|
||||
/* 2 */
|
||||
border-top-width: 1px;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct text decoration in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
abbr:where([title]) {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the default font size and weight for headings.
|
||||
*/
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Reset links to optimize for opt-in styling instead of opt-out.
|
||||
*/
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font weight in Edge and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use the user's configured `mono` font family by default.
|
||||
2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
/* 1 */
|
||||
font-size: 1em;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
|
||||
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
|
||||
3. Remove gaps between table borders by default.
|
||||
*/
|
||||
|
||||
table {
|
||||
text-indent: 0;
|
||||
/* 1 */
|
||||
border-color: inherit;
|
||||
/* 2 */
|
||||
border-collapse: collapse;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Change the font styles in all browsers.
|
||||
2. Remove the margin in Firefox and Safari.
|
||||
3. Remove default padding in all browsers.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
/* 1 */
|
||||
font-feature-settings: inherit;
|
||||
/* 1 */
|
||||
font-variation-settings: inherit;
|
||||
/* 1 */
|
||||
font-size: 100%;
|
||||
/* 1 */
|
||||
font-weight: inherit;
|
||||
/* 1 */
|
||||
line-height: inherit;
|
||||
/* 1 */
|
||||
color: inherit;
|
||||
/* 1 */
|
||||
margin: 0;
|
||||
/* 2 */
|
||||
padding: 0;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inheritance of text transform in Edge and Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Remove default button styles.
|
||||
*/
|
||||
|
||||
button,
|
||||
[type='button'],
|
||||
[type='reset'],
|
||||
[type='submit'] {
|
||||
-webkit-appearance: button;
|
||||
/* 1 */
|
||||
background-color: transparent;
|
||||
/* 2 */
|
||||
background-image: none;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Use the modern Firefox focus style for all focusable elements.
|
||||
*/
|
||||
|
||||
:-moz-focusring {
|
||||
outline: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
|
||||
*/
|
||||
|
||||
:-moz-ui-invalid {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct vertical alignment in Chrome and Firefox.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/*
|
||||
Correct the cursor style of increment and decrement buttons in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-inner-spin-button,
|
||||
::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the odd appearance in Chrome and Safari.
|
||||
2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type='search'] {
|
||||
-webkit-appearance: textfield;
|
||||
/* 1 */
|
||||
outline-offset: -2px;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button;
|
||||
/* 1 */
|
||||
font: inherit;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct display in Chrome and Safari.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/*
|
||||
Removes the default spacing and border for appropriate elements.
|
||||
*/
|
||||
|
||||
blockquote,
|
||||
dl,
|
||||
dd,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
hr,
|
||||
figure,
|
||||
p,
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
menu {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Reset default styling for dialogs.
|
||||
*/
|
||||
|
||||
dialog {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent resizing textareas horizontally by default.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
|
||||
2. Set the default placeholder color to the user's configured gray 400 color.
|
||||
*/
|
||||
|
||||
input::-moz-placeholder, textarea::-moz-placeholder {
|
||||
opacity: 1;
|
||||
/* 1 */
|
||||
color: #9ca3af;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
opacity: 1;
|
||||
/* 1 */
|
||||
color: #9ca3af;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Set the default cursor for buttons.
|
||||
*/
|
||||
|
||||
button,
|
||||
[role="button"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/*
|
||||
Make sure disabled buttons don't get the pointer cursor.
|
||||
*/
|
||||
|
||||
:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
|
||||
This can trigger a poorly considered lint error in some tools but is included by design.
|
||||
*/
|
||||
|
||||
img,
|
||||
svg,
|
||||
video,
|
||||
canvas,
|
||||
audio,
|
||||
iframe,
|
||||
embed,
|
||||
object {
|
||||
display: block;
|
||||
/* 1 */
|
||||
vertical-align: middle;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
*/
|
||||
|
||||
img,
|
||||
video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Make elements with the HTML hidden attribute stay hidden by default */
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
*, ::before, ::after {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-gradient-from-position: ;
|
||||
--tw-gradient-via-position: ;
|
||||
--tw-gradient-to-position: ;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
}
|
||||
|
||||
::backdrop {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-gradient-from-position: ;
|
||||
--tw-gradient-via-position: ;
|
||||
--tw-gradient-to-position: ;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.container {
|
||||
max-width: 640px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
max-width: 768px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.container {
|
||||
max-width: 1024px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.container {
|
||||
max-width: 1280px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1536px) {
|
||||
.container {
|
||||
max-width: 1536px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: 0.125rem;
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(37 99 235 / var(--tw-bg-opacity));
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
padding-left: 0.875rem;
|
||||
padding-right: 0.875rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
font-weight: 600;
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.visible {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.collapse {
|
||||
visibility: collapse;
|
||||
}
|
||||
|
||||
.absolute {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.m-2 {
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
.mx-auto {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.ml-auto {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.mt-3 {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.mr-2 {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.mt-2 {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.mt-6 {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.mt-10 {
|
||||
margin-top: 2.5rem;
|
||||
}
|
||||
|
||||
.mt-16 {
|
||||
margin-top: 4rem;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.h-1 {
|
||||
height: 0.25rem;
|
||||
}
|
||||
|
||||
.h-8 {
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.h-12 {
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
.h-24 {
|
||||
height: 6rem;
|
||||
}
|
||||
|
||||
.h-20 {
|
||||
height: 5rem;
|
||||
}
|
||||
|
||||
.w-1 {
|
||||
width: 0.25rem;
|
||||
}
|
||||
|
||||
.w-8 {
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
.w-12 {
|
||||
width: 3rem;
|
||||
}
|
||||
|
||||
.w-36 {
|
||||
width: 9rem;
|
||||
}
|
||||
|
||||
.w-32 {
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
.w-24 {
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
.w-20 {
|
||||
width: 5rem;
|
||||
}
|
||||
|
||||
.max-w-2xl {
|
||||
max-width: 42rem;
|
||||
}
|
||||
|
||||
.max-w-6xl {
|
||||
max-width: 72rem;
|
||||
}
|
||||
|
||||
.transform {
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.resize {
|
||||
resize: both;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.rounded-sm {
|
||||
border-radius: 0.125rem;
|
||||
}
|
||||
|
||||
.border {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.fill-blue-600 {
|
||||
fill: #2563eb;
|
||||
}
|
||||
|
||||
.p-10 {
|
||||
padding: 2.5rem;
|
||||
}
|
||||
|
||||
.p-20 {
|
||||
padding: 5rem;
|
||||
}
|
||||
|
||||
.p-4 {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.p-40 {
|
||||
padding: 10rem;
|
||||
}
|
||||
|
||||
.p-60 {
|
||||
padding: 15rem;
|
||||
}
|
||||
|
||||
.p-80 {
|
||||
padding: 20rem;
|
||||
}
|
||||
|
||||
.py-8 {
|
||||
padding-top: 2rem;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-2xl {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
.text-3xl {
|
||||
font-size: 1.875rem;
|
||||
line-height: 2.25rem;
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.text-gray-700 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(55 65 81 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-gray-200 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(229 231 235 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.transition {
|
||||
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
|
||||
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
|
||||
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
[x-cloak] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
margin-top: 0.125rem;
|
||||
margin-bottom: 0.125rem;
|
||||
width: 100%;
|
||||
border-radius: 0.125rem;
|
||||
border-width: 1px;
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
input {
|
||||
padding-left: 0.375rem;
|
||||
padding-right: 0.375rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:text-gray-600 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(75 85 99 / var(--tw-text-opacity));
|
||||
}
|
||||
}
|
||||
121
public/css/dygraphs.css
Normal file
121
public/css/dygraphs.css
Normal file
@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Default styles for the dygraphs charting library.
|
||||
*/
|
||||
|
||||
.dygraph-legend {
|
||||
position: absolute;
|
||||
font-size: 14px;
|
||||
z-index: 10;
|
||||
width: 250px; /* labelsDivWidth */
|
||||
/*
|
||||
dygraphs determines these based on the presence of chart labels.
|
||||
It might make more sense to create a wrapper div around the chart proper.
|
||||
top: 0px;
|
||||
right: 2px;
|
||||
*/
|
||||
background: white;
|
||||
line-height: normal;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dygraph-legend[dir="rtl"] {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* styles for a solid line in the legend */
|
||||
.dygraph-legend-line {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
bottom: .5ex;
|
||||
padding-left: 1em;
|
||||
height: 1px;
|
||||
border-bottom-width: 2px;
|
||||
border-bottom-style: solid;
|
||||
/* border-bottom-color is set based on the series color */
|
||||
}
|
||||
|
||||
/* styles for a dashed line in the legend, e.g. when strokePattern is set */
|
||||
.dygraph-legend-dash {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
bottom: .5ex;
|
||||
height: 1px;
|
||||
border-bottom-width: 2px;
|
||||
border-bottom-style: solid;
|
||||
/* border-bottom-color is set based on the series color */
|
||||
/* margin-right is set based on the stroke pattern */
|
||||
/* padding-left is set based on the stroke pattern */
|
||||
}
|
||||
|
||||
.dygraph-roller {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* This class is shared by all annotations, including those with icons */
|
||||
.dygraph-annotation {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* This class only applies to annotations without icons */
|
||||
/* Old class name: .dygraphDefaultAnnotation */
|
||||
.dygraph-default-annotation {
|
||||
border: 1px solid black;
|
||||
background-color: white;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dygraph-axis-label {
|
||||
/* position: absolute; */
|
||||
/* font-size: 14px; */
|
||||
z-index: 10;
|
||||
line-height: normal;
|
||||
overflow: hidden;
|
||||
color: black; /* replaces old axisLabelColor option */
|
||||
}
|
||||
|
||||
.dygraph-axis-label-x {
|
||||
}
|
||||
|
||||
.dygraph-axis-label-y {
|
||||
}
|
||||
|
||||
.dygraph-axis-label-y2 {
|
||||
}
|
||||
|
||||
.dygraph-title {
|
||||
font-weight: bold;
|
||||
z-index: 10;
|
||||
text-align: center;
|
||||
/* font-size: based on titleHeight option */
|
||||
}
|
||||
|
||||
.dygraph-xlabel {
|
||||
text-align: center;
|
||||
/* font-size: based on xLabelHeight option */
|
||||
}
|
||||
|
||||
/* For y-axis label */
|
||||
.dygraph-label-rotate-left {
|
||||
text-align: center;
|
||||
/* See http://caniuse.com/#feat=transforms2d */
|
||||
transform: rotate(90deg);
|
||||
-webkit-transform: rotate(90deg);
|
||||
-moz-transform: rotate(90deg);
|
||||
-o-transform: rotate(90deg);
|
||||
-ms-transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* For y2-axis label */
|
||||
.dygraph-label-rotate-right {
|
||||
text-align: center;
|
||||
/* See http://caniuse.com/#feat=transforms2d */
|
||||
transform: rotate(-90deg);
|
||||
-webkit-transform: rotate(-90deg);
|
||||
-moz-transform: rotate(-90deg);
|
||||
-o-transform: rotate(-90deg);
|
||||
-ms-transform: rotate(-90deg);
|
||||
}
|
||||
26
public/css/main.css
Normal file
26
public/css/main.css
Normal file
@ -0,0 +1,26 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer components {
|
||||
.btn {
|
||||
@apply bg-blue-600 text-white py-2 px-3.5 font-semibold text-sm rounded-sm hover:opacity-70;
|
||||
}
|
||||
}
|
||||
|
||||
[x-cloak] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
@apply w-full border py-1 rounded-sm my-0.5 bg-white;
|
||||
}
|
||||
|
||||
input {
|
||||
@apply px-1.5;
|
||||
}
|
||||
|
||||
hr {
|
||||
@apply my-2;
|
||||
}
|
||||
99
public/index.html
Normal file
99
public/index.html
Normal file
@ -0,0 +1,99 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Modbus Logger App</title>
|
||||
<link rel="stylesheet" href="/css/dist.css" />
|
||||
<script defer type="module" src="/js/app.js"></script>
|
||||
</head>
|
||||
<body class="m-2">
|
||||
<!-- Root element -->
|
||||
<div x-data x-cloak class="mx-auto max-w-6xl m-2 py-8">
|
||||
<!-- Monitoring content -->
|
||||
<main x-bind:hidden="$store.app.currentPage !== 'monitoring'">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold">Main page</h1>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Configuration content -->
|
||||
<main x-bind:hidden="$store.app.currentPage !== 'configuration'">
|
||||
<div x-data="configAppModal" class="border rounded-sm p-4 max-w-2xl mx-auto">
|
||||
<h2 class="text-2xl font-bold">Setup configuration</h2>
|
||||
<p class="text-gray-700 italic">
|
||||
Fill in the required configuration settings to connect to the Modbus network.
|
||||
</p>
|
||||
<hr />
|
||||
<form @submit.prevent="configurateApp()" class="flex flex-col" action="#">
|
||||
<div>
|
||||
<label class="block" for="conn_type">Network type</label>
|
||||
<select name="conn_type" id="conn_type">
|
||||
<option value="TCP">TCP</option>
|
||||
<option value="RTU">RTU</option>
|
||||
</select>
|
||||
|
||||
<div>
|
||||
<label>IP</label>
|
||||
<input placeholder="e.g 127.0.0.1" type="text" />
|
||||
|
||||
<label>Port</label>
|
||||
<input placeholder="e.g 502" type="text" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Path</label>
|
||||
<input placeholder="e.g COM1" type="text" />
|
||||
|
||||
<label>Baud</label>
|
||||
<input placeholder="e.g 9600" type="text" />
|
||||
|
||||
<label>Parity</label>
|
||||
<select name="parity" id="conn_type">
|
||||
<option value="even">even</option>
|
||||
<option value="odd">odd</option>
|
||||
<option value="none">none</option>
|
||||
</select>
|
||||
|
||||
<label>Data bits</label>
|
||||
<input placeholder="e.g 8" type="text" />
|
||||
|
||||
<label>Stop bits</label>
|
||||
<input placeholder="e.g 1" type="text" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 ml-auto">
|
||||
<button class="btn" type="submit">Connect</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Loading content -->
|
||||
<main x-bind:hidden="$store.app.currentPage !== 'loading'">
|
||||
<div class="text-center mt-16">
|
||||
<div role="status">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="inline w-20 h-20 mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="mt-4">Loading...</p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
5
public/js/alpine.js
Normal file
5
public/js/alpine.js
Normal file
File diff suppressed because one or more lines are too long
29
public/js/app.js
Normal file
29
public/js/app.js
Normal file
@ -0,0 +1,29 @@
|
||||
// @ts-nocheck
|
||||
import "./alpine.js";
|
||||
|
||||
document.addEventListener("alpine:init", async () => {
|
||||
Alpine.store("app", {
|
||||
is_configured: false,
|
||||
config: null,
|
||||
currentPage: "loading",
|
||||
async init() {
|
||||
const { config } = await (await fetch("/api/app/config")).json();
|
||||
const { is_configured } = await (await fetch("/api/app/is_configured")).json();
|
||||
this.is_configured = is_configured;
|
||||
this.config = config;
|
||||
|
||||
if (is_configured) {
|
||||
this.currentPage = "monitoring";
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentPage = "configuration";
|
||||
},
|
||||
});
|
||||
|
||||
Alpine.data("configAppModal", () => ({
|
||||
async configurateApp() {
|
||||
console.log("Configuration app...");
|
||||
},
|
||||
}));
|
||||
});
|
||||
3
public/js/dygraphs.min.js
vendored
Normal file
3
public/js/dygraphs.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
152
routes/api.router.js
Normal file
152
routes/api.router.js
Normal file
@ -0,0 +1,152 @@
|
||||
import express from "express";
|
||||
import db from "#root/config/database.config.js";
|
||||
import modbusClient from "#root/config/modbus-client.config.js";
|
||||
|
||||
/**
|
||||
* Validators
|
||||
*/
|
||||
import { appConfigSchema } from "#root/validators/index.js";
|
||||
|
||||
/**
|
||||
* Services
|
||||
*/
|
||||
import { appService } from "#root/services/index.js";
|
||||
|
||||
/**
|
||||
* Middlewares
|
||||
*/
|
||||
import { validateBodyMiddleware } from "#root/middlewares/index.js";
|
||||
|
||||
const apiRouter = express.Router();
|
||||
|
||||
/**
|
||||
* Get application config
|
||||
*/
|
||||
apiRouter.get("/app/config", async (req, res, next) => {
|
||||
try {
|
||||
const data = await appService.getConfig(db);
|
||||
if (!data) return res.status(404).json({ config: null });
|
||||
return res.status(200).json({ config: data });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Update app configuration
|
||||
*/
|
||||
apiRouter.patch("/app/config", validateBodyMiddleware(appConfigSchema), async (req, res, next) => {
|
||||
try {
|
||||
const config = await appService.getConfig(db);
|
||||
const payload = req.body;
|
||||
|
||||
if (!config) await db.run(`INSERT INTO "app_config" (id) VALUES (?)`, [0]);
|
||||
|
||||
/* Update TCP configuration */
|
||||
if (payload.mb_connection_type === "TCP") {
|
||||
const { mb_tcp_ip, mb_tcp_port } = payload;
|
||||
await db.run(
|
||||
`
|
||||
UPDATE "app_config" SET id = 0, mb_connection_type = "TCP", mb_tcp_ip = ?, mb_tcp_port = ?, log_interval_ms = ?
|
||||
`,
|
||||
[mb_tcp_ip, mb_tcp_port, payload.log_interval_ms],
|
||||
);
|
||||
|
||||
return res.status(200).send("Updated");
|
||||
}
|
||||
|
||||
/* Update RTU configuration */
|
||||
if (payload.mb_connection_type === "RTU") {
|
||||
const { mb_rtu_path, mb_rtu_baud, mb_rtu_parity, mb_rtu_data_bits, mb_rtu_stop_bits } = payload;
|
||||
await db.run(
|
||||
`
|
||||
UPDATE "app_config" SET id = 0,
|
||||
mb_connection_type = "RTU",
|
||||
mb_rtu_path = ?,
|
||||
mb_rtu_baud = ?,
|
||||
mb_rtu_parity = ?,
|
||||
mb_rtu_data_bits = ?,
|
||||
mb_rtu_stop_bits = ?,
|
||||
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("Updated.");
|
||||
}
|
||||
|
||||
return res.status(400).send("Undefined Modbus connection type.");
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Check whether the application is configured or not
|
||||
*/
|
||||
apiRouter.get("/app/is_configured", async (req, res, next) => {
|
||||
try {
|
||||
const config = await appService.getConfig(db);
|
||||
let is_configured = false;
|
||||
|
||||
if (!config) return res.status(200).send({ is_configured });
|
||||
|
||||
const isValuesExists = (colPrefix) => {
|
||||
const keys = Object.keys(config).filter((k) => k.includes(colPrefix));
|
||||
for (const k of keys) if (!config[k]) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
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 });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Connect to modbus network using configuration from database
|
||||
*/
|
||||
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 (modbusClient.isOpen) {
|
||||
modbusClient.close(undefined);
|
||||
modbusClient.destroy(undefined);
|
||||
}
|
||||
|
||||
if (config.mb_connection_type === "TCP") {
|
||||
await modbusClient.connectTCP(config.mb_tcp_ip, { port: config.mb_tcp_port });
|
||||
return res.status(200).send("Connected.");
|
||||
}
|
||||
|
||||
if (config.mb_connection_type === "RTU") {
|
||||
await modbusClient.connectRTU(config.mb_rtu_path, {
|
||||
baudRate: config.mb_rtu_baud,
|
||||
dataBits: config.mb_rtu_data_bits,
|
||||
parity: config.mb_rtu_parity,
|
||||
stopBits: config.mb_rtu_stop_bits,
|
||||
});
|
||||
return res.status(200).send("Connected.");
|
||||
}
|
||||
|
||||
return res.status(400).send("Connection type is invalid.");
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
apiRouter.post("/modbus/close", (req, res, next) => {
|
||||
try {
|
||||
if (!modbusClient.isOpen) return res.status(400).send("Already closed.");
|
||||
modbusClient.close(undefined);
|
||||
modbusClient.destroy(undefined);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
export { apiRouter };
|
||||
1
routes/index.js
Normal file
1
routes/index.js
Normal file
@ -0,0 +1 @@
|
||||
export * from './api.router.js';
|
||||
7
scripts/db-init.script.js
Normal file
7
scripts/db-init.script.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { runQueryFromFile } from "#root/utils/database.util.js";
|
||||
|
||||
async function run() {
|
||||
await runQueryFromFile("sql/init-schema.sql");
|
||||
}
|
||||
|
||||
run();
|
||||
4
server.js
Normal file
4
server.js
Normal file
@ -0,0 +1,4 @@
|
||||
import app from "#root/app/index.js";
|
||||
import env from "#root/config/env.config.js";
|
||||
|
||||
app.listen(env.SERVER_PORT, () => console.log(`Web server started: http://127.0.0.1:${env.SERVER_PORT}.`));
|
||||
22
services/app.service.js
Normal file
22
services/app.service.js
Normal file
@ -0,0 +1,22 @@
|
||||
export const appService = {
|
||||
/**
|
||||
* Get configuration of app
|
||||
* @param {import('sqlite').Database} db
|
||||
* @returns {Promise<import('../index').AppConfig | undefined>}
|
||||
*/
|
||||
async getConfig(db) {
|
||||
const data = await db.get(`
|
||||
SELECT "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"
|
||||
FROM "app_config"
|
||||
`);
|
||||
return data;
|
||||
},
|
||||
};
|
||||
1
services/index.js
Normal file
1
services/index.js
Normal file
@ -0,0 +1 @@
|
||||
export * from "./app.service.js";
|
||||
15
sql/init-schema.sql
Normal file
15
sql/init-schema.sql
Normal file
@ -0,0 +1,15 @@
|
||||
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,
|
||||
"log_interval_ms" INTEGER DEFAULT 1000 NOT NULL,
|
||||
CONSTRAINT "id_unique" CHECK(id=0),
|
||||
CONSTRAINT "min_interval_check" CHECK(log_interval_ms >= 1000)
|
||||
PRIMARY KEY("id")
|
||||
);
|
||||
8
tailwind.config.js
Normal file
8
tailwind.config.js
Normal file
@ -0,0 +1,8 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./public/**/*.{html,js}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
15
utils/database.util.js
Normal file
15
utils/database.util.js
Normal file
@ -0,0 +1,15 @@
|
||||
import fs from "fs/promises";
|
||||
import db from "#root/config/database.config.js";
|
||||
|
||||
/**
|
||||
* Reads the `.sql` file and executes the query
|
||||
* @param {string} filepath
|
||||
*/
|
||||
export async function runQueryFromFile(filepath) {
|
||||
try {
|
||||
const query = await fs.readFile(filepath);
|
||||
await db.run(query.toString());
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
18
validators/index.js
Normal file
18
validators/index.js
Normal file
@ -0,0 +1,18 @@
|
||||
import { object, number, string } from "yup";
|
||||
|
||||
const requiredIfConnTypeEquals = (type) => ({
|
||||
is: (v) => v === type,
|
||||
then: (schema) => schema.required(),
|
||||
});
|
||||
|
||||
export const appConfigSchema = object({
|
||||
mb_connection_type: string().oneOf(["TCP", "RTU"]).required(),
|
||||
mb_tcp_ip: string().when("mb_connection_type", requiredIfConnTypeEquals("TCP")),
|
||||
mb_tcp_port: number().when("mb_connection_type", requiredIfConnTypeEquals("TCP")),
|
||||
mb_rtu_path: string().when("mb_connection_type", requiredIfConnTypeEquals("RTU")),
|
||||
mb_rtu_baud: number().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_stop_bits: number().oneOf([1, 2]).when("mb_connection_type", requiredIfConnTypeEquals("RTU")),
|
||||
log_interval_ms: number().min(1000).required(),
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user