Add modbus dashboard

This commit is contained in:
cpq 2023-11-27 02:50:46 +00:00
parent 30738f6638
commit 64ca1ad787
21 changed files with 3003 additions and 7 deletions

View File

@ -0,0 +1,55 @@
PROG ?= ./example # Program we are building
PACK ?= ./pack # Packing executable
DELETE = rm -rf # Command to remove files
GZIP ?= gzip # For compressing files in web_root/
OUT ?= -o $(PROG) # Compiler argument for output file
SOURCES = main.c mongoose.c net.c packed_fs.c # Source code files
CFLAGS = -W -Wall -Wextra -g -I. # Build options
# Mongoose build options. See https://mongoose.ws/documentation/#build-options
CFLAGS_MONGOOSE += -DMG_ENABLE_PACKED_FS=1
ifeq ($(OS),Windows_NT) # Windows settings. Assume MinGW compiler. To use VC: make CC=cl CFLAGS=/MD OUT=/Feprog.exe
PROG = example.exe # Use .exe suffix for the binary
PACK = pack.exe # Packing executable
CC = gcc # Use MinGW gcc compiler
CFLAGS += -lws2_32 # Link against Winsock library
DELETE = cmd /C del /Q /F /S # Command prompt command to delete files
GZIP = echo # No gzip on Windows
endif
# Default target. Build and run program
all: $(PROG)
$(RUN) $(PROG) $(ARGS)
# Build program from sources
$(PROG): $(SOURCES)
$(CC) $(SOURCES) $(CFLAGS) $(CFLAGS_MONGOOSE) $(CFLAGS_EXTRA) $(OUT)
# Bundle JS libraries (preact, preact-router, ...) into a single file
web_root/bundle.js:
curl -s https://npm.reversehttp.com/preact,preact/hooks,htm/preact,preact-router -o $@
# Create optimised CSS. Prerequisite: npm -g i tailwindcss tailwindcss-font-inter
web_root/main.css: web_root/index.html $(wildcard web_root/*.js)
npx tailwindcss -o $@ --minify
# Generate packed filesystem for serving Web UI
packed_fs.c: $(wildcard web_root/*) $(wildcard certs/*) Makefile web_root/main.css web_root/bundle.js
$(GZIP) web_root/*
$(CC) ../../test/pack.c -o $(PACK)
$(PACK) web_root/* certs/* > $@
$(GZIP) -d web_root/*
mbedtls:
git clone --depth 1 -b v2.28.2 https://github.com/mbed-tls/mbedtls $@
ifeq ($(TLS), mbedtls)
CFLAGS += -DMG_TLS=MG_TLS_MBED -Wno-conversion -Imbedtls/include
CFLAGS += -DMBEDTLS_CONFIG_FILE=\"mbedtls_config.h\" mbedtls/library/*.c
$(PROG): mbedtls
endif
# Cleanup. Delete built program and all build artifacts
clean:
$(DELETE) $(PROG) $(PACK) *.o *.obj *.exe *.dSYM mbedtls

View File

@ -0,0 +1,26 @@
# A complete device dashboard
This example is a demonstration of how Mongoose Library could be integrated
into an embedded device and provide a complete device dashboard with the
following features:
- Authentication: login-protected dashboard
- Multiple logins (with possibly different permissions)
- The Web UI can be fully embedded into the firmware binary, then not
needing a filesystem to serve it; so being resilient to FS problems
- All changes are propagated to all connected clients
## Screenshots
This is a login screen that prompts for user/password
![](screenshots/login.webp)
## Main dashboard
The main dashboard page shows the interactive console
![](screenshots/dashboard.webp)
See a detailed tutorial at https://mongoose.ws/tutorials/device-dashboard/

View File

@ -0,0 +1 @@
../device-dashboard/certs

View File

@ -0,0 +1,32 @@
// Copyright (c) 2020-2023 Cesanta Software Limited
// All rights reserved
#include "mongoose.h"
#include "net.h"
static int s_sig_num;
static void signal_handler(int sig_num) {
signal(sig_num, signal_handler);
s_sig_num = sig_num;
}
int main(void) {
struct mg_mgr mgr;
signal(SIGPIPE, SIG_IGN);
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
mg_log_set(MG_LL_DEBUG); // Set debug log level
mg_mgr_init(&mgr);
web_init(&mgr);
while (s_sig_num == 0) {
mg_mgr_poll(&mgr, 50);
}
mg_mgr_free(&mgr);
MG_INFO(("Exiting on signal %d", s_sig_num));
return 0;
}

View File

@ -0,0 +1 @@
../../mongoose.c

View File

@ -0,0 +1 @@
../../mongoose.h

View File

@ -0,0 +1,295 @@
// Copyright (c) 2023 Cesanta Software Limited
// All rights reserved
#include "net.h"
// Device settings
struct settings {
int log_level;
bool mqtt_enabled;
char *mqtt_server_url;
char *mqtt_topic_tx;
char *mqtt_topic_rx;
};
struct conndata {
uint64_t expiration_time; // Modbus request timeout
unsigned long id; // Connection ID waiting for the Modbus response
};
static struct settings s_settings = {3, false, NULL, NULL, NULL};
static const char *s_json_header =
"Content-Type: application/json\r\n"
"Cache-Control: no-cache\r\n";
static uint64_t s_boot_timestamp = 0; // Updated by SNTP
// This is for newlib and TLS (mbedTLS)
uint64_t mg_now(void) {
return mg_millis() + s_boot_timestamp;
}
// SNTP connection event handler. When we get a response from an SNTP server,
// adjust s_boot_timestamp. We'll get a valid time from that point on
static void sfn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
uint64_t *expiration_time = (uint64_t *) c->data;
if (ev == MG_EV_OPEN) {
*expiration_time = mg_millis() + 3000; // Store expiration time in 3s
} else if (ev == MG_EV_SNTP_TIME) {
uint64_t t = *(uint64_t *) ev_data;
s_boot_timestamp = t - mg_millis();
c->is_closing = 1;
} else if (ev == MG_EV_POLL) {
if (mg_millis() > *expiration_time) c->is_closing = 1;
}
(void) fn_data;
}
// SNTP timer function. Sync up time
static void timer_sntp_fn(void *param) {
mg_sntp_connect(param, "udp://time.google.com:123", sfn, NULL);
}
static void setfromjson(struct mg_str json, const char *jsonpath, char **dst) {
char *val = mg_json_get_str(json, jsonpath);
if (val != NULL) {
free(*dst);
*dst = val;
}
}
static void handle_settings_set(struct mg_connection *c, struct mg_str body) {
struct settings settings;
memset(&settings, 0, sizeof(settings));
mg_json_get_bool(body, "$.mqtt_enabled", &settings.mqtt_enabled);
settings.log_level = mg_json_get_long(body, "$.log_level", MG_LL_INFO);
setfromjson(body, "$.mqtt_server_url", &settings.mqtt_server_url);
setfromjson(body, "$.mqtt_topic_rx", &settings.mqtt_topic_rx);
setfromjson(body, "$.mqtt_topic_tx", &settings.mqtt_topic_tx);
s_settings = settings; // TODO: save to the device flash
bool ok = true;
mg_http_reply(c, 200, s_json_header,
"{%m:%s,%m:%m}", //
MG_ESC("status"), ok ? "true" : "false", //
MG_ESC("message"), MG_ESC(ok ? "Success" : "Failed"));
}
static void handle_settings_get(struct mg_connection *c) {
mg_http_reply(c, 200, s_json_header,
"{%m:%s,%m:%d,%m:%m,%m:%m,%m:%m}\n", //
MG_ESC("mqtt_enabled"),
s_settings.mqtt_enabled ? "true" : "false", //
MG_ESC("log_level"), s_settings.log_level, //
MG_ESC("mqtt_server_url"), MG_ESC(s_settings.mqtt_server_url),
MG_ESC("mqtt_topic_rx"), MG_ESC(s_settings.mqtt_topic_rx),
MG_ESC("mqtt_topic_tx"), MG_ESC(s_settings.mqtt_topic_tx));
}
// Modbus handler function
static void mfn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
struct conndata *cd = (struct conndata *) c->data;
if (ev == MG_EV_READ) {
MG_INFO(("%lu RECEIVED %lu", c->id, c->recv.len));
if (c->recv.len < 8) return; // Less than minimum length, buffer more
uint16_t len = mg_ntohs(*(uint16_t *) &c->recv.buf[4]); // PDU length
if (c->recv.len < len + 4U) return; // Partial frame, buffer more
// Notify parent connection
for (struct mg_connection *t = c->mgr->conns; t != NULL; t = t->next) {
if (t->id == cd->id) mg_call(t, MG_EV_USER, &c->recv);
}
c->is_closing = 1;
} else if (MG_EV_POLL) {
// MG_INFO(("%lu closing tmout %llu", c->id, cd->expiration_time));
if (cd->expiration_time > 0 && cd->expiration_time < mg_millis()) {
c->is_closing = 1;
}
}
(void) ev_data, (void) fn_data;
}
static void send8(struct mg_connection *c, uint8_t val) {
mg_send(c, &val, sizeof(val));
}
static void send16(struct mg_connection *c, uint16_t val) {
send8(c, (uint8_t) (val >> 8) & 255);
send8(c, (uint8_t) (val & 255));
}
static struct mg_connection *start_modbus_request(struct mg_mgr *mgr,
struct mg_str json,
unsigned long cid) {
struct mg_connection *c = NULL;
char *url = mg_json_get_str(json, "$.url");
long timeout = mg_json_get_long(json, "$.timeout", 750);
uint8_t id = (uint8_t) mg_json_get_long(json, "$.id", 1);
uint16_t reg = (uint16_t) mg_json_get_long(json, "$.reg", 1);
uint8_t func = (uint8_t) mg_json_get_long(json, "$.func", 0);
uint16_t nregs = (uint16_t) mg_json_get_long(json, "$.nregs", 1);
MG_INFO(("%lu REQUEST: %.*s", cid, json.len, json.ptr));
if (func == 0) {
MG_ERROR(("Set func to a valid modbus function code"));
} else if ((c = mg_connect(mgr, url, mfn, NULL)) == NULL) {
MG_ERROR(("Failed to start modbus connection at %M", MG_ESC(url)));
} else {
send16(c, 1); // TID. Use random?
send16(c, 0); // Protocol identifier: 0 (modbus)
uint16_t *lp = (uint16_t *) &c->send.buf[c->send.len];
send16(c, 0); // Length: to be set later
size_t len = c->send.len;
send8(c, id); // Client ID
send8(c, func); // Function
if (func == 3 || func == 4 || func == 6 || func == 16) {
send16(c, reg); // Start register
send16(c, nregs); // Number of registers
if (func == 16) { // Fill in register values to write
send8(c, (uint8_t) (nregs * 2)); // Send number of bytes
for (uint16_t i = 0; i < nregs; i++) {
char path[20];
mg_snprintf(path, sizeof(path), "$.values[%hu]", i);
uint16_t r = (uint16_t) mg_json_get_long(json, path, 0);
send16(c, r);
}
}
} else if (func == 43) {
send8(c, 14); // MEI
send8(c, 1); // Basic device ID: 1
send8(c, 0); // Object ID: 0 (vendor name)
}
*lp = mg_htons((uint16_t) (c->send.len - len)); // Set length field
mg_hexdump(c->send.buf, c->send.len);
MG_INFO(("%lu SENDING %lu", c->id, c->send.len));
struct conndata *cd = (struct conndata *) c->data;
cd->id = cid; // Store parent connection ID
cd->expiration_time = mg_millis() + timeout;
}
free(url);
return c;
}
static void handle_modbus_exec(struct mg_connection *c, struct mg_str body) {
struct mg_connection *mc = start_modbus_request(c->mgr, body, c->id);
if (mc == NULL) {
mg_http_reply(c, 200, s_json_header, "false\n");
} else {
struct conndata *cd = (struct conndata *) c->data;
cd->expiration_time = mg_millis() + 1500;
}
}
// Print modbus response
static size_t print_mb_resp(void (*out)(char, void *), void *ptr, va_list *ap) {
int func = va_arg(*ap, int);
size_t len = va_arg(*ap, size_t);
uint8_t *buf = va_arg(*ap, uint8_t *);
size_t n = 0;
if (func == 3 || func == 4) {
// For "multiple" read responses, print registers
n += mg_xprintf(out, ptr, "[");
for (size_t ofs = 1; ofs < len; ofs += 2) {
uint16_t reg = mg_ntohs(*(uint8_t *) &buf[ofs]);
n += mg_xprintf(out, ptr, "%s%hu", ofs == 1 ? "" : ",", reg);
}
n += mg_xprintf(out, ptr, "]");
} else if (func == 43) {
n += mg_xprintf(out, ptr, "{");
if (len >= 6) {
n += mg_xprintf(out, ptr, "%m:%d", MG_ESC("mei"), buf[0]);
n += mg_xprintf(out, ptr, ",%m:%d", MG_ESC("devid"), buf[1]);
n += mg_xprintf(out, ptr, ",%m:%d", MG_ESC("conformity"), buf[2]);
n += mg_xprintf(out, ptr, ",%m:%d", MG_ESC("more"), buf[3]);
n += mg_xprintf(out, ptr, ",%m:%d", MG_ESC("next"), buf[4]);
n += mg_xprintf(out, ptr, ",%m:[", MG_ESC("objects"));
size_t ofs = 6;
while (ofs < len) {
if (ofs > 6) n += mg_xprintf(out, ptr, ",");
n += mg_xprintf(out, ptr, "{");
n += mg_xprintf(out, ptr, "%m:%d", MG_ESC("id"), buf[ofs++]);
if (ofs < len) {
uint8_t olen = buf[ofs++];
if (olen > (uint8_t) (len - ofs)) olen = (uint8_t) (len - ofs);
n += mg_xprintf(out, ptr, ",%m:%m", MG_ESC("value"), mg_print_esc,
olen, &buf[ofs]);
ofs += olen;
}
n += mg_xprintf(out, ptr, "}");
}
n += mg_xprintf(out, ptr, "]");
}
n += mg_xprintf(out, ptr, "}");
} else {
n += mg_xprintf(out, ptr, "null");
}
return n;
}
// HTTP request handler function
static void fn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
struct conndata *cd = (struct conndata *) c->data;
if (ev == MG_EV_ACCEPT) {
if (fn_data != NULL) { // TLS listener!
struct mg_tls_opts opts = {0};
opts.cert = mg_unpacked("/certs/server_cert.pem");
opts.key = mg_unpacked("/certs/server_key.pem");
mg_tls_init(c, &opts);
}
} else if (ev == MG_EV_HTTP_MSG) {
struct mg_http_message *hm = (struct mg_http_message *) ev_data;
if (mg_http_match_uri(hm, "/api/settings/get")) {
handle_settings_get(c);
} else if (mg_http_match_uri(hm, "/api/settings/set")) {
handle_settings_set(c, hm->body);
} else if (mg_http_match_uri(hm, "/api/settings/set")) {
handle_settings_set(c, hm->body);
} else if (mg_http_match_uri(hm, "/api/modbus/exec")) {
handle_modbus_exec(c, hm->body);
} else if (mg_http_match_uri(hm, "/api/device/reset")) {
mg_timer_add(c->mgr, 500, 0, (void (*)(void *)) mg_device_reset, NULL);
mg_http_reply(c, 200, s_json_header, "true\n");
} else {
struct mg_http_serve_opts opts;
memset(&opts, 0, sizeof(opts));
#if MG_ARCH == MG_ARCH_UNIX || MG_ARCH == MG_ARCH_WIN32
opts.root_dir = "web_root"; // On workstations, use filesystem
#else
opts.root_dir = "/web_root"; // On embedded, use packed files
opts.fs = &mg_fs_packed;
#endif
mg_http_serve_dir(c, ev_data, &opts);
}
MG_DEBUG(("%lu %.*s %.*s", c->id, (int) hm->method.len, hm->method.ptr,
(int) hm->uri.len, hm->uri.ptr));
} else if (ev == MG_EV_POLL) {
if (cd->expiration_time > 0 && cd->expiration_time < mg_millis()) {
cd->expiration_time = 0;
mg_http_reply(c, 200, s_json_header, "false\n");
}
} else if (ev == MG_EV_USER) {
cd->expiration_time = 0; // Cleanup timeout setting
struct mg_iobuf *io = ev_data;
uint8_t f = io->buf[7];
mg_http_reply(c, 200, s_json_header, "{%m:%s,%m:%m,%m:%M}\n", //
MG_ESC("success"), "true", //
MG_ESC("raw"), mg_print_hex, io->len - 7, io->buf + 7, //
MG_ESC("data"), print_mb_resp, f, io->len - 8, io->buf + 8);
}
}
void web_init(struct mg_mgr *mgr) {
// Init default settings
s_settings.mqtt_server_url = strdup("mqtt://broker.hivemq.com:1883");
s_settings.mqtt_topic_tx = strdup("modbus1/tx");
s_settings.mqtt_topic_rx = strdup("modbus1/rx");
mg_http_listen(mgr, HTTP_URL, fn, NULL);
mg_http_listen(mgr, HTTPS_URL, fn, (void *) 1);
mg_timer_add(mgr, 10 * 60 * 1000, MG_TIMER_RUN_NOW | MG_TIMER_REPEAT,
timer_sntp_fn, mgr);
}

View File

@ -0,0 +1,23 @@
// Copyright (c) 2023 Cesanta Software Limited
// All rights reserved
#pragma once
#include "mongoose.h"
#ifdef __cplusplus
extern "C" {
#endif
#if !defined(HTTP_URL)
#define HTTP_URL "http://0.0.0.0:8000"
#endif
#if !defined(HTTPS_URL)
#define HTTPS_URL "https://0.0.0.0:8443"
#endif
void web_init(struct mg_mgr *mgr);
#ifdef __cplusplus
}
#endif

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,17 @@
module.exports = {
content: ['./web_root/*.{html,js}'],
xplugins: [ 'tailwindcss', 'xautoprefixer' ],
corePlugins: {outline: false},
theme: {
extend: {},
fontFamily: {
sans:
[
"Inter var, Helvetica, sans-serif", {
fontFeatureSettings: '"cv11", "ss01"',
fontVariationSettings: '"opsz" 32',
}
]
}
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en" class="h-full bg-white">
<head>
<title></title>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='currentColor'> <path stroke-linecap='round' stroke-linejoin='round' d='M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0' /> </svg>" />
<link href="main.css" rel="stylesheet" />
<link href="https://rsms.me/inter/inter.css" rel="stylesheet" />
</head>
<body class="h-full"></body>
<script src="history.min.js"></script>
<script type="module" src="main.js"></script>
</html>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,365 @@
// NOTE: API calls must start with 'api/' in order to serve the app at any URI
'use strict';
import { h, render, useState, useEffect, useRef, html, Router } from './bundle.js';
const Icons = {
cog: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> </svg>`,
info: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" /> </svg>`,
bolt: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" /> </svg>`,
home: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" /> </svg> `,
save: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M16.5 3.75V16.5L12 14.25 7.5 16.5V3.75m9 0H18A2.25 2.25 0 0120.25 6v12A2.25 2.25 0 0118 20.25H6A2.25 2.25 0 013.75 18V6A2.25 2.25 0 016 3.75h1.5m9 0h-9" /> </svg>`,
bars3: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" /> </svg>`,
};
function TextValue({value, setfn, disabled, placeholder, type, addonRight, addonLeft, attr, min, max, step, mult}) {
const [bg, setBg] = useState('bg-white');
useEffect(() => { if (type == 'number') checkval(+min, +max, +value); }, []);
step ||= '1', mult ||= 1;
const checkval = function(min, max, v) {
setBg('bg-white');
if (min && v < min) setBg('bg-red-100 border-red-200');
if (max && v > max) setBg('bg-red-100 border-red-200');
};
const m = step.match(/^.+\.(.+)/);
const digits = m ? m[1].length : 0;
const onchange = ev => {
let v = ev.target.value;
if (type == 'number') {
checkval(+min, +max, +v);
v = +(parseFloat(v) / mult).toFixed(digits);
}
setfn(v);
};
if (type == 'number') value = +(value * mult).toFixed(digits);
return html`
<div class="flex w-full items-center rounded border shadow-sm ${bg}">
${addonLeft && html`<span class="inline-flex font-normal truncate py-1 border-r bg-slate-100 items-center border-gray-300 px-2 text-gray-500 text-xs">${addonLeft}<//>` }
<input type=${type || 'text'} disabled=${disabled} value=${value}
step=${step} min=${min} max=${max}
onchange=${onchange} ...${attr}
class="${bg} font-normal text-sm rounded w-full flex-1 py-0.5 px-2 text-gray-700 placeholder:text-gray-400 focus:outline-none disabled:cursor-not-allowed disabled:bg-gray-100 disabled:text-gray-500" placeholder=${placeholder} />
${addonRight && html`<span class="inline-flex font-normal truncate py-1 border-l bg-slate-100 items-center border-gray-300 px-2 text-gray-500 text-xs overflow-scroll" style="min-width: 50%;">${addonRight}<//>` }
<//>`;
};
function SelectValue({value, setfn, options, disabled}) {
const toInt = x => x == parseInt(x) ? parseInt(x) : x;
const onchange = ev => setfn(toInt(ev.target.value));
return html`
<select onchange=${onchange} class="w-full rounded font-normal border py-0.5 px-1 text-gray-600 focus:outline-none text-sm disabled:cursor-not-allowed" disabled=${disabled}>
${options.map(v => html`<option value=${v[0]} selected=${v[0] == value}>${v[1]}<//>`) }
<//>`;
};
function SwitchValue({value, setfn}) {
const onclick = ev => setfn(!value);
const bg = !!value ? 'bg-blue-600' : 'bg-gray-200';
const tr = !!value ? 'translate-x-5' : 'translate-x-0';
return html`
<button type="button" onclick=${onclick} class="${bg} inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-0 ring-0" role="switch" aria-checked=${!!value}>
<span aria-hidden="true" class="${tr} pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 focus:ring-0 transition duration-200 ease-in-out"></span>
</button>`;
};
function Setting(props) {
let input = TextValue;
if (props.type == 'switch') input = SwitchValue;
if (props.type == 'select') input = SelectValue;
return html`
<div class=${props.cls || 'grid grid-cols-2 gap-2 my-1'}>
<label class="flex items-center text-sm text-gray-700 mr-2 font-medium">${props.title}<//>
<div class="flex items-center">${h(input, props)}<//>
<//>`;
};
function Button({title, onclick, disabled, cls, icon, ref, colors, hovercolor, disabledcolor}) {
const [spin, setSpin] = useState(false);
const cb = function(ev) {
const res = onclick ? onclick() : null;
if (res && typeof (res.catch) === 'function') {
setSpin(true);
res.catch(() => false).then(() => setSpin(false));
}
};
if (!colors) colors = 'bg-blue-600 hover:bg-blue-500 disabled:bg-blue-400';
return html`
<button type="button" class="inline-flex justify-center items-center gap-2 rounded px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm ${colors} ${cls}"
ref=${ref} onclick=${cb} disabled=${disabled || spin} >
${title}
<${spin ? Icons.refresh : icon} class="w-4 ${spin ? 'animate-spin' : ''}" />
<//>`
};
function Notification({ok, text, close}) {
const closebtn = useRef(null);
const from = 'translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2';
const to = 'translate-y-0 opacity-100 sm:translate-x-0';
const [tr, setTr] = useState(from);
useEffect(function() {
setTr(to);
setTimeout(ev => closebtn && closebtn.current.click && closebtn.current.click(), 1500);
}, []);
const onclose = ev => { setTr(from); setTimeout(close, 300); };
return html`
<div aria-live="assertive" class="z-10 pointer-events-none absolute inset-0 flex items-end px-4 py-6 sm:items-start sm:p-6">
<div class="flex w-full flex-col items-center space-y-4 sm:items-end">
<div class="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5 transform ease-out duration-300 transition ${tr}">
<div class="p-4">
<div class="flex items-start">
<div class="flex-shrink-0">
<${ok ? Icons.ok : Icons.fail} class="h-6 w-6 ${ok ? 'text-green-400' : 'text-red-400'}" />
<//>
<div class="ml-3 w-0 flex-1 pt-0.5">
<p class="text-sm font-medium text-gray-900">${text}</p>
<p class="hidden mt-1 text-sm text-gray-500">Anyone with a link can now view this file.</p>
<//>
<div class="ml-4 flex flex-shrink-0">
<button type="button" ref=${closebtn} onclick=${onclose} class="inline-flex rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none">
<span class="sr-only">Close</span>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
<//>
<//>
<//>
<//>
<//>
<//>
<//>
<//>`;
};
const Logo = props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13 13"><defs><style>.ll-cls-1{fill:none;stroke:#000;stroke-miterlimit:10;stroke-width:0.5px;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="ll-cls-1" d="M12.62,1.82V8.91A1.58,1.58,0,0,1,11,10.48H4a1.44,1.44,0,0,1-1-.37A.69.69,0,0,1,2.84,10l-.1-.12a.81.81,0,0,1-.15-.48V5.57a.87.87,0,0,1,.86-.86H4.73V7.28a.86.86,0,0,0,.86.85H9.42a.85.85,0,0,0,.85-.85V3.45A.86.86,0,0,0,10.13,3,.76.76,0,0,0,10,2.84a.29.29,0,0,0-.12-.1,1.49,1.49,0,0,0-1-.37H2.39V1.82A1.57,1.57,0,0,1,4,.25H11A1.57,1.57,0,0,1,12.62,1.82Z"/><path class="ll-cls-1" d="M10.48,10.48V11A1.58,1.58,0,0,1,8.9,12.6H1.82A1.57,1.57,0,0,1,.25,11V3.94A1.57,1.57,0,0,1,1.82,2.37H8.9a1.49,1.49,0,0,1,1,.37l.12.1a.76.76,0,0,1,.11.14.86.86,0,0,1,.14.47V7.28a.85.85,0,0,1-.85.85H8.13V5.57a.86.86,0,0,0-.85-.86H3.45a.87.87,0,0,0-.86.86V9.4a.81.81,0,0,0,.15.48l.1.12a.69.69,0,0,0,.13.11,1.44,1.44,0,0,0,1,.37Z"/></g></g></svg>`;
const ModbusLogo = props => html`
<svg version="1.1" class=${props.class} xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 652 195">
<style type="text/css"> .mb_st0{fill:#F49500;} .mb_st1{fill:#FCC000;} .mb_st2{fill:#03A239;} .mb_st3{fill:#1192CF;} </style>
<g transform="translate(0 -225)">
<path class="mb_st0" d="M189.4,323.1c0-36.5-31-66.1-69.3-66.1c-38.3,0-69.3,29.6-69.3,66.1c0,36.5,31,66.1,69.3,66.1 C158.4,389.1,189.4,359.6,189.4,323.1"/>
<path class="mb_st1" d="M127.3,251.6c0,14.2-11.5,25.8-25.8,25.8c-14.2,0-25.8-11.5-25.8-25.8c0-14.2,11.5-25.8,25.8-25.8 C115.7,225.8,127.3,237.4,127.3,251.6"/>
<path class="mb_st1" d="M198.9,273.5c0,14.2-11.5,25.8-25.8,25.8c-14.2,0-25.8-11.5-25.8-25.8c0-14.2,11.5-25.8,25.8-25.8 C187.4,247.7,198.9,259.3,198.9,273.5"/>
<path class="mb_st1" d="M223.5,341.5c0,14.2-11.5,25.8-25.8,25.8c-14.2,0-25.8-11.5-25.8-25.8c0-14.2,11.5-25.8,25.8-25.8 C212,315.8,223.5,327.3,223.5,341.5"/>
<path class="mb_st1" d="M168.9,391.3c0,14.2-11.5,25.8-25.8,25.8c-14.2,0-25.8-11.5-25.8-25.8c0-14.2,11.5-25.8,25.8-25.8 C157.3,365.5,168.9,377,168.9,391.3"/>
<path class="mb_st1" d="M94.3,373.4c0,14.2-11.5,25.8-25.8,25.8c-14.2,0-25.8-11.5-25.8-25.8c0-14.2,11.5-25.8,25.8-25.8 C82.7,347.6,94.3,359.2,94.3,373.4"/>
<path class="mb_st1" d="M73.5,303.1c0,14.2-11.5,25.8-25.8,25.8c-14.2,0-25.8-11.5-25.8-25.8c0-14.2,11.5-25.8,25.8-25.8 C61.9,277.3,73.5,288.9,73.5,303.1"/>
<polygon class="mb_st2" points="125.3,308 123.4,301.4 119.4,309 105.1,259.6 112.3,263.8 110.5,257.5 100.5,252 94.9,262 96.8,268.3 100.6,260.9 115,310.3 107.6,306 109.5,312.6 119.6,318 "/> <polygon class="mb_st2" points="137.7,320.1 142.1,316 134.1,315.4 167.7,284.3 167.7,292.1 171.9,288.2 171.7,277.3 160.9,276.3 156.7,280.3 164.5,280.8 130.9,312 131,303.9 126.5,308.1 126.8,319 "/>
<polygon class="mb_st2" points="82.1,373.4 86.6,368.9 78.4,368.7 112.1,335.5 112.2,343.5 116.5,339.3 116.2,328.2 105.1,327.7 100.9,331.9 108.8,332.1 75.1,365.4 75,357.2 70.5,361.6 71,372.7 "/>
<polygon class="mb_st2" points="108.7,312.2 102.3,310.2 106.4,317.6 58.1,302.8 65.4,299 59.3,297.1 49.3,302.5 54.6,312.6 60.7,314.5 56.8,307.3 105,322.1 97.5,325.9 103.9,327.9 113.9,322.4 "/>
<polygon class="mb_st2" points="147.9,378.9 145.9,373 142.1,380.2 127.4,336 134.6,339.3 132.7,333.7 122.6,329.3 117.3,338.8 119.2,344.4 122.9,337.5 137.7,381.7 130.3,378.2 132.3,384.1 142.4,388.5 "/>
<polygon class="mb_st2" points="183.6,332.6 177.6,330.8 181.3,338.1 136.5,325.1 143.3,321.1 137.7,319.5 128.3,325.2 133.2,335 138.8,336.7 135.2,329.6 180,342.5 173,346.6 179,348.4 188.3,342.5 "/>
<polygon class="mb_st3" points="129.6,357.5 143.8,286.8 176.2,286.8 190.8,329.3 222.5,286.8 255,286.8 240.6,357.5 220.6,357.5 230.4,307.5 193.3,357.5 177.6,357.5 159.9,307.6 150.1,357.5 "/>
<path class="mb_st3" d="M320.1,310.8c-2.8-2.8-12.6-4-24-4c-17.8,0-26.6,2.9-31.9,8.2c-5.3,5.3-7.9,25.3-7.9,27.5 c0,2.1-0.8,7.8,3.1,11.7c3.9,3.9,13.3,4.2,27.8,4.2c14.5,0,22.5-2.5,27.9-6.2c5.4-3.6,8.6-22.7,9.2-28.6 C324.8,317.6,322.9,313.5,320.1,310.8 M305.9,333.1c-1.5,5.3-2.1,7.5-3.9,8.8c-1.8,1.3-11,2.2-13,2.2c-2,0-11.8-0.2-13.8-2.2 c-2-2,1-16.9,3.9-18.5c2.9-1.6,8.9-2.1,13.2-2c6.6,0.1,11.6,0.3,13,1.7C306.7,324.7,307.4,327.8,305.9,333.1"/>
<path class="mb_st3" d="M389,286.8l-5.5,28.1c-2.6-6.2-7.5-8.1-18-8.1c-17.8,0-20.9,2.9-26.2,8.2s-7.9,25.3-7.9,27.5 c0,2.1-0.8,7.8,3.1,11.7c3.9,3.9,7,4.2,21.1,4.2c13.6,0,18.5-4.6,20.9-7.7l-1.3,6.8h17.8l13.8-70.7H389z M376,342 c-1.8,1.3-11,2.2-13.1,2.2s-11.8-0.2-13.8-2.2c-2-2,1-16.9,3.9-18.5c2.9-1.6,8.9-2.1,13.2-2c6.6,0.1,11.6,0.3,13.1,1.7 c0.9,0.9,1.5,2.6,1.4,5.1l-1.2,5.9C378.3,338.8,377.7,340.8,376,342"/>
<path class="mb_st3" d="M472.3,309.8c-2.2-1.9-7.8-3-18.4-3c-13.3,0-18.8,2.7-23,8.4l5.5-28.3H419L405,357.5h17.7l1.4-7.2 c2.1,4.9,5.3,8.1,20,8.1c14.5,0,18.4-0.7,23-5.4c4.9-4.9,8.7-23.4,9.2-29.4C476.8,317.7,476,312.9,472.3,309.8 M458.4,333.1 c-1.5,5.3-2.1,7.5-3.9,8.8c-1.8,1.3-11,2.2-13,2.2c-2,0-11.8-0.2-13.8-2.2c-2-2,1-16.9,3.9-18.5c2.9-1.6,8.9-2.1,13.2-2 c6.6,0.1,11.6,0.3,13,1.7C459.3,324.7,459.9,327.8,458.4,333.1"/>
<path class="mb_st3" d="M536.2,307.4h-0.7c0,0-4.7,27.5-9,31.8c-4.3,4.3-10.6,4.6-12.8,4.6c-2.1,0-8.2-0.9-10.4-3.1 c-2.7-2.8,3.2-28.7,4.3-33.3h-17.3c0,0-7,31.7-7,36.6c0,3.9,0.4,11.1,11.2,13.6c2.5,0.6,5.5,0.9,9.2,0.9c2.9,0,5.6-0.3,8-0.9 c8.4-2,13.7-6.8,16.6-10.4l-2,10.4h16.5l9.8-50.1h0H536.2z"/>
<path class="mb_st3" d="M605.6,323h16.1c0,0,0.3-3.9,0.3-5.5c0-1.7,0.3-3.9-3.1-7.3c-3.4-3.4-21.8-3.3-24.9-3.3 c-3.1,0-19.8-0.3-24.5,3.6c-4,3.2-7.3,7.4-7.3,15.6c0,8.3,5.5,11.5,16.4,11.5h19.5c2.7,0,6.1,0.8,6.1,3.3c0,2.5-1.9,5.9-8.7,5.9 h-12.9c-3.1,0-8.7-0.5-8.7-4v-1.6h-16.1c0,0-0.8,2.8-0.8,5.2c0,2.4,0.4,5.5,3.1,8.2c2.7,2.7,14,3.9,25.6,3.9 c11.6,0,24.1-0.5,28.3-4.8c4.3-4.3,6.2-12.3,6.2-16.5c0-4.2-1.4-6.2-2.8-7.6c-1.3-1.3-5.3-3.7-23-3.7c-15.8,0-16.3-1.5-16.3-3 c0-1.2,0.2-4.2,12.7-4.2C603.3,318.6,605.6,319.2,605.6,323"/>
</g>
</svg>`;
function Header({logout, user, setShowSidebar, showSidebar}) {
return html`
<div class="bg-white sticky top-0 z-[48] xw-full border-b py-2 ${showSidebar && 'pl-72'} transition-all duration-300 transform">
<div class="px-2 w-full py-0 my-0 flex items-center">
<button type="button" onclick=${ev => setShowSidebar(v => !v)} class="text-slate-400">
<${Icons.bars3} class="h-6" />
<//>
<div class="flex flex-1 gap-x-4 self-stretch lg:gap-x-6">
<div class="relative flex flex-1"><//>
<div class="flex items-center gap-x-4 lg:gap-x-6">
<span class="text-sm text-slate-400">developed with Mongoose Library<//>
<div class="hidden lg:block lg:h-4 lg:w-px lg:bg-gray-200" aria-hidden="true"><//>
<span><${ModbusLogo} class="h-10" /><//>
<//>
<//>
<//>
<//>`;
};
function Sidebar({url, show}) {
const NavLink = ({title, icon, href, url}) => html`
<div>
<a href="#${href}" class="${href == url ? 'bg-slate-50 text-blue-600 group' : 'text-gray-700 hover:text-blue-600 hover:bg-gray-50 group'} flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold">
<${icon} class="w-6 h-6"/>
${title}
<///>
<//>`;
return html`
<div class="bg-violet-100 hs-overlay hs-overlay-open:translate-x-0
-translate-x-full transition-all duration-300 transform
fixed top-0 left-0 bottom-0 z-[60] w-72 bg-white border-r
border-gray-200 overflow-y-auto scrollbar-y
${show && 'translate-x-0'} right-auto bottom-0">
<div class="flex flex-col m-4 gap-y-6">
<div class="flex h-10 shrink-0 items-center gap-x-4 font-bold text-xl text-slate-500">
<${Logo} class="h-full"/> Your Brand
<//>
<div class="flex flex-1 flex-col">
<${NavLink} title="Dashboard" icon=${Icons.home} href="/" url=${url} />
<${NavLink} title="Settings" icon=${Icons.cog} href="/settings" url=${url} />
<//>
<//>
<//>`;
};
function DeveloperNote({text, children}) {
return html`
<div class="flex p-4 gap-2">
<div class="text-sm text-slate-500">
<div class="flex items-center">
<${Icons.info} class="self-start basis-[30px] grow-0 shrink-0 text-green-600 mr-2" />
<div class="font-semibold">Developer Note<//>
<//>
${(text || '').split('.').map(v => html` <p class="my-2 ">${v}<//>`)}
${children}
<//>
<//>`;
};
function Main({}) {
const [slaveUrl, setSlaveUrl] = useState('tcp://192.168.0.100:502');
//const [slaveUrl, setSlaveUrl] = useState('tcp://127.0.0.1:10502');
const [slaveId, setSlaveId] = useState(1);
const [startReg, setStartReg] = useState(0);
const [values, setValues] = useState('0');
const [numRegs, setNumRegs] = useState(1);
const [func, setFunc] = useState(3);
const [connectTimeout, setConnectTimeout] = useState(1500);
const [response, setResponse] = useState('');
const onexec = ev => (setResponse('') || 1) && fetch('api/modbus/exec' , {
method: 'post', body: JSON.stringify({
url: slaveUrl, id: slaveId, reg: startReg,
values: (values || '0').split(',').map(v => parseInt(v)),
timeout: connectTimeout, func: func, nregs: numRegs
})
}).then(r => r.json())
.then(r => setResponse(JSON.stringify(r, null, 2)));
const funcs = [
[3, '3: Read Multiple Holding Registers'],
[4, '4: Read Multiple Input Registers'],
// [6, '6: Write Single Holding Register'],
[16, '16: Write Multiple Holding Registers'],
[43, '43: Read Device ID'],
];
const setnumregs = function(val) { // Number of registers changed
setValues(Array(val).fill(0).join(',')); // Values: zero-filled array
return setNumRegs(val); // And set the number of regs
};
return html`
<div class="m-4 grid grid-cols-1 gap-4 md:grid-cols-1">
<div class="py-1 divide-y border rounded bg-white flex flex-col">
<div class="font-light uppercase flex items-center text-gray-600 px-4 py-2">
Modbus Master Console
<//>
<div class="py-2 px-4 grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="flex-1 flex flex-col relative">
<${Setting} title="Slave URL" value=${slaveUrl} setfn=${val => setSlaveUrl(val)} type="" />
<${Setting} title="Connect timeout" value=${connectTimeout} setfn=${val => setConnectTimeout(val)} type="number" addonRight="milliseconds" min="50" max="5000"/>
<${Setting} title="Slave ID" value=${slaveId} setfn=${val => setSlaveId(val)} type="number" min="1" max="253" addonRight="1 .. 253" />
<${Setting} title="Starting Register" value=${startReg} setfn=${val => setStartReg(val)} type="number" min="0" max="65535" addonRight="0 .. 65535" />
<${Setting} title="Register Count" value=${numRegs} setfn=${setnumregs} type="number" min="1" max="125" addonRight="1 .. 125" />
<${Setting} title="Value(s), e.g. 1 or 1,2,3" value=${values} setfn=${val => setValues(val)} addonRight="1 .. 65535" cls=${func == 16 || func == 6 ? '' : 'hidden'} />
<${Setting} title="Function" value=${func} setfn=${val => setFunc(val)} type="select" options=${funcs} />
<div class="mb-1 mt-3 flex place-content-end gap-2 ">
<${Button} icon=${Icons.bolt} onclick=${onexec} title="Execute" />
<//>
<//>
<div class="flex-1 flex flex-col relative">
<pre class="flex-1 bg-gray-100 p-2 text-sm overflow-auto max-h-64">${response}<//>
<//>
<//>
<//>
<//>`;
};
function Settings({}) {
const [settings, setSettings] = useState(null);
const [saveResult, setSaveResult] = useState(null);
const refresh = () => fetch('api/settings/get')
.then(r => r.json())
.then(r => setSettings(r));
useEffect(refresh, []);
const mksetfn = k => (v => setSettings(x => Object.assign({}, x, {[k]: v})));
const onsave = ev => fetch('api/settings/set', {
method: 'post', body: JSON.stringify(settings)
}).then(r => r.json())
.then(r => setSaveResult(r))
.then(refresh);
if (!settings) return '';
const logOptions = [[0, 'Disable'], [1, 'Error'], [2, 'Info'], [3, 'Debug']];
return html`
<div class="m-4 grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="py-1 divide-y border rounded bg-white flex flex-col">
<div class="font-light uppercase flex items-center text-gray-600 px-4 py-2">
MQTT Forwarding
<//>
<div class="py-2 px-5 flex-1 flex flex-col relative">
${saveResult && html`<${Notification} ok=${saveResult.status}
text=${saveResult.message} close=${() => setSaveResult(null)} />`}
<${Setting} title="Enable MQTT forwarding" value=${settings.mqtt_enabled} setfn=${mksetfn('mqtt_enabled')} type="switch" />
<${Setting} title="MQTT Server URL" value=${settings.mqtt_server_url} setfn=${mksetfn('mqtt_server_url')} type="" disabled=${!settings.mqtt_enabled} />
<${Setting} title="MQTT Topic RX" value=${settings.mqtt_topic_rx} setfn=${mksetfn('mqtt_topic_rx')} type="" disabled=${!settings.mqtt_enabled} />
<${Setting} title="MQTT Topic TX" value=${settings.mqtt_topic_tx} setfn=${mksetfn('mqtt_topic_tx')} type="" disabled=${!settings.mqtt_enabled} />
<div class="mb-1 mt-3 flex place-content-end"><${Button} icon=${Icons.save} onclick=${onsave} title="Save Settings" /><//>
<//>
<//>
<div class="bg-white border rounded-md text-ellipsis overflow-auto" role="alert">
<${DeveloperNote}>
<div>
MQTT forwarding sends MQTT message to the TX topic every time
a Modbus slave was successfully interrogated, in either manual
or automatic mode.
<//>
<div class="mt-2">
The RX topic can be used to send Modbus commands over MQTT. The
command should be a JSON message of the following format:
<//>
<div class="mt-2">
<code>{"url": "tcp://IP:PORT", "reg": NUM, "id": NUM, "func": NUM}<//>
<//>
<//>
<//>
<div class="py-1 divide-y border rounded bg-white flex flex-col">
<div class="font-light uppercase flex items-center text-gray-600 px-4 py-2">
Console Log
<//>
<div class="py-2 px-5 flex-1 flex flex-col relative">
${saveResult && html`<${Notification} ok=${saveResult.status}
text=${saveResult.message} close=${() => setSaveResult(null)} />`}
<${Setting} title="Log Level" value=${settings.log_level} setfn=${mksetfn('log_level')} type="select" addonLeft="0-3" options=${logOptions} />
<div class="mb-1 mt-3 flex place-content-end"><${Button} icon=${Icons.save} onclick=${onsave} title="Save Settings" /><//>
<//>
<//>
<//>`;
};
const App = function({}) {
const [url, setUrl] = useState('/');
const [showSidebar, setShowSidebar] = useState(true);
return html`
<div class="min-h-screen bg-slate-100">
<${Sidebar} url=${url} show=${showSidebar} />
<${Header} showSidebar=${showSidebar} setShowSidebar=${setShowSidebar} />
<div class="${showSidebar && 'pl-72'} transition-all duration-300 transform">
<${Router} onChange=${ev => setUrl(ev.url)} history=${History.createHashHistory()} >
<${Main} default=${true} />
<${Settings} path="settings" />
<//>
<//>
<//>`;
};
window.onload = () => render(h(App), document.body);

View File

@ -1,13 +1,14 @@
CFLAGS = -W -Wall -Wextra -Wno-error -Wundef -Wshadow -Wdouble-promotion
CFLAGS += -Wformat-truncation -fno-common -Wconversion -Wno-sign-conversion
CFLAGS += -ffunction-sections -fdata-sections #-fmessage-length=0 -fsigned-char
CFLAGS += -march=rv32imafc -mabi=ilp32f #-msmall-data-limit=8 -mno-save-restore
CFLAGS += -ffunction-sections -fdata-sections
CFLAGS += -march=rv32imafc -mabi=ilp32f
CFLAGS += -DSYSCLK_FREQ_144MHz_HSE -I. -Ivendor -g3 -Os $(CFLAGS_EXTRA)
LDFLAGS = -T vendor/link.ld -nostartfiles --specs=nano.specs #--specs=nosys.specs
LDFLAGS += -lc -lgcc -Wl,--gc-sections #-Wl,-Map=$@.map
LDFLAGS = -T vendor/link.ld -nostartfiles --specs=nano.specs
LDFLAGS += -lc -lgcc -Wl,--gc-sections
SOURCES = main.c mongoose.c vendor/system_ch32v30x.c vendor/startup_ch32v30x_D8C.S
SOURCES = main.c mongoose.c net.c packed_fs.c vendor/system_ch32v30x.c vendor/startup_ch32v30x_D8C.S
CFLAGS += -DHTTP_URL=\"http://0.0.0.0/\" -DHTTPS_URL=\"https://0.0.0.0/\"
ifeq ($(OS),Windows_NT)
RM = cmd /C del /Q /F /S

View File

@ -2,6 +2,7 @@
#include "hal.h"
#include "mongoose.h"
#include "net.h"
#define BTN_PIN PIN('B', 3) // On-board user button
#define LED1_PIN PIN('A', 15) // On-board red LED
@ -105,7 +106,8 @@ int main(void) {
}
MG_INFO(("Initialising application..."));
mg_http_listen(&mgr, "http://0.0.0.0", fn, NULL);
mg_http_listen(&mgr, "http://0.0.0.0:8000", fn, NULL);
web_init(&mgr);
MG_INFO(("Starting event loop"));
for (;;) {

View File

@ -6,7 +6,8 @@
#define MG_ENABLE_TCPIP 1
#define MG_ENABLE_CUSTOM_MILLIS 1
#define MG_ENABLE_CUSTOM_RANDOM 1
//#define MG_ENABLE_PACKED_FS 1
#define MG_ENABLE_PACKED_FS 1
#define MG_ENABLE_FILE 0
#define MG_ENABLE_DRIVER_STM32 1
#define MG_ENABLE_LINES 1
#define MG_IO_SIZE 256

View File

@ -0,0 +1 @@
../../modbus-dashboard/net.c

View File

@ -0,0 +1 @@
../../modbus-dashboard/net.h

View File

@ -0,0 +1 @@
../../modbus-dashboard/packed_fs.c