mirror of
https://github.com/cesanta/mongoose.git
synced 2026-10-17 10:49:47 +00:00
Add modbus dashboard
This commit is contained in:
1 parent
30738f6638
commit
64ca1ad787
21 files changed
+3003
-7
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
## Main dashboard
|
||||
|
||||
The main dashboard page shows the interactive console
|
||||
|
||||

|
||||
|
||||
|
||||
See a detailed tutorial at https://mongoose.ws/tutorials/device-dashboard/
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../device-dashboard/certs
|
||||
@@ -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;
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../mongoose.c
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../mongoose.h
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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',
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
var n,t,e,_,r,o,i,u,l,c={},a=[],s=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;function f(n,t){for(var e in t)n[e]=t[e];return n}function p(n){var t=n.parentNode;t&&t.removeChild(n)}function h(t,e,_){var r,o,i,u={};for(i in e)"key"==i?r=e[i]:"ref"==i?o=e[i]:u[i]=e[i];if(arguments.length>2&&(u.children=arguments.length>3?n.call(arguments,2):_),"function"==typeof t&&null!=t.defaultProps)for(i in t.defaultProps)void 0===u[i]&&(u[i]=t.defaultProps[i]);return d(t,u,r,o,null)}function d(n,_,r,o,i){var u={type:n,props:_,key:r,ref:o,__k:null,__:null,__b:0,__e:null,__d:void 0,__c:null,__h:null,constructor:void 0,__v:null==i?++e:i};return null==i&&null!=t.vnode&&t.vnode(u),u}function v(){return{current:null}}function m(n){return n.children}function y(n,t){this.props=n,this.context=t}function g(n,t){if(null==t)return n.__?g(n.__,n.__.__k.indexOf(n)+1):null;for(var e;t<n.__k.length;t++)if(null!=(e=n.__k[t])&&null!=e.__e)return e.__e;return"function"==typeof n.type?g(n):null}function b(n){var t,e;if(null!=(n=n.__)&&null!=n.__c){for(n.__e=n.__c.base=null,t=0;t<n.__k.length;t++)if(null!=(e=n.__k[t])&&null!=e.__e){n.__e=n.__c.base=e.__e;break}return b(n)}}function k(n){(!n.__d&&(n.__d=!0)&&r.push(n)&&!C.__r++||o!==t.debounceRendering)&&((o=t.debounceRendering)||i)(C)}function C(){var n,t,e,_,o,i,l,c;for(r.sort(u);n=r.shift();)n.__d&&(t=r.length,_=void 0,o=void 0,l=(i=(e=n).__v).__e,(c=e.__P)&&(_=[],(o=f({},i)).__v=i.__v+1,T(c,i,o,e.__n,void 0!==c.ownerSVGElement,null!=i.__h?[l]:null,_,null==l?g(i):l,i.__h),R(_,i),i.__e!=l&&b(i)),r.length>t&&r.sort(u));C.__r=0}function x(n,t,e,_,r,o,i,u,l,s){var f,p,h,v,y,b,k,C=_&&_.__k||a,x=C.length;for(e.__k=[],f=0;f<t.length;f++)if(null!=(v=e.__k[f]=null==(v=t[f])||"boolean"==typeof v||"function"==typeof v?null:"string"==typeof v||"number"==typeof v||"bigint"==typeof v?d(null,v,null,null,v):Array.isArray(v)?d(m,{children:v},null,null,null):v.__b>0?d(v.type,v.props,v.key,v.ref?v.ref:null,v.__v):v)){if(v.__=e,v.__b=e.__b+1,null===(h=C[f])||h&&v.key==h.key&&v.type===h.type)C[f]=void 0;else for(p=0;p<x;p++){if((h=C[p])&&v.key==h.key&&v.type===h.type){C[p]=void 0;break}h=null}T(n,v,h=h||c,r,o,i,u,l,s),y=v.__e,(p=v.ref)&&h.ref!=p&&(k||(k=[]),h.ref&&k.push(h.ref,null,v),k.push(p,v.__c||y,v)),null!=y?(null==b&&(b=y),"function"==typeof v.type&&v.__k===h.__k?v.__d=l=E(v,l,n):l=U(n,v,h,C,y,l),"function"==typeof e.type&&(e.__d=l)):l&&h.__e==l&&l.parentNode!=n&&(l=g(h))}for(e.__e=b,f=x;f--;)null!=C[f]&&("function"==typeof e.type&&null!=C[f].__e&&C[f].__e==e.__d&&(e.__d=A(_).nextSibling),W(C[f],C[f]));if(k)for(f=0;f<k.length;f++)M(k[f],k[++f],k[++f])}function E(n,t,e){for(var _,r=n.__k,o=0;r&&o<r.length;o++)(_=r[o])&&(_.__=n,t="function"==typeof _.type?E(_,t,e):U(e,_,_,r,_.__e,t));return t}function H(n,t){return t=t||[],null==n||"boolean"==typeof n||(Array.isArray(n)?n.some((function(n){H(n,t)})):t.push(n)),t}function U(n,t,e,_,r,o){var i,u,l;if(void 0!==t.__d)i=t.__d,t.__d=void 0;else if(null==e||r!=o||null==r.parentNode)n:if(null==o||o.parentNode!==n)n.appendChild(r),i=null;else{for(u=o,l=0;(u=u.nextSibling)&&l<_.length;l+=1)if(u==r)break n;n.insertBefore(r,o),i=o}return void 0!==i?i:r.nextSibling}function A(n){var t,e,_;if(null==n.type||"string"==typeof n.type)return n.__e;if(n.__k)for(t=n.__k.length-1;t>=0;t--)if((e=n.__k[t])&&(_=A(e)))return _;return null}function P(n,t,e,_,r){var o;for(o in e)"children"===o||"key"===o||o in t||N(n,o,null,e[o],_);for(o in t)r&&"function"!=typeof t[o]||"children"===o||"key"===o||"value"===o||"checked"===o||e[o]===t[o]||N(n,o,t[o],e[o],_)}function S(n,t,e){"-"===t[0]?n.setProperty(t,null==e?"":e):n[t]=null==e?"":"number"!=typeof e||s.test(t)?e:e+"px"}function N(n,t,e,_,r){var o;n:if("style"===t)if("string"==typeof e)n.style.cssText=e;else{if("string"==typeof _&&(n.style.cssText=_=""),_)for(t in _)e&&t in e||S(n.style,t,"");if(e)for(t in e)_&&e[t]===_[t]||S(n.style,t,e[t])}else if("o"===t[0]&&"n"===t[1])o=t!==(t=t.replace(/Capture$/,"")),t=t.toLowerCase()in n?t.toLowerCase().slice(2):t.slice(2),n.l||(n.l={}),n.l[t+o]=e,e?_||n.addEventListener(t,o?D:w,o):n.removeEventListener(t,o?D:w,o);else if("dangerouslySetInnerHTML"!==t){if(r)t=t.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if("width"!==t&&"height"!==t&&"href"!==t&&"list"!==t&&"form"!==t&&"tabIndex"!==t&&"download"!==t&&t in n)try{n[t]=null==e?"":e;break n}catch(n){}"function"==typeof e||(null==e||!1===e&&"-"!==t[4]?n.removeAttribute(t):n.setAttribute(t,e))}}function w(n){return this.l[n.type+!1](t.event?t.event(n):n)}function D(n){return this.l[n.type+!0](t.event?t.event(n):n)}function T(n,e,_,r,o,i,u,l,c){var a,s,p,h,d,v,g,b,k,C,E,H,U,A,P,S=e.type;if(void 0!==e.constructor)return null;null!=_.__h&&(c=_.__h,l=e.__e=_.__e,e.__h=null,i=[l]),(a=t.__b)&&a(e);try{n:if("function"==typeof S){if(b=e.props,k=(a=S.contextType)&&r[a.__c],C=a?k?k.props.value:a.__:r,_.__c?g=(s=e.__c=_.__c).__=s.__E:("prototype"in S&&S.prototype.render?e.__c=s=new S(b,C):(e.__c=s=new y(b,C),s.constructor=S,s.rLine truncated
|
||||
@@ -0,0 +1 @@
|
||||
!function(t,n){"object"==typeof exports&&"object"==typeof module?module.exports=n():"function"==typeof define&&define.amd?define([],n):"object"==typeof exports?exports.History=n():t.History=n()}(this,function(){return function(t){function n(o){if(e[o])return e[o].exports;var r=e[o]={exports:{},id:o,loaded:!1};return t[o].call(r.exports,r,r.exports,n),r.loaded=!0,r.exports}var e={};return n.m=t,n.c=e,n.p="",n(0)}([function(t,n,e){"use strict";function o(t){return t&&t.__esModule?t:{default:t}}n.__esModule=!0,n.createPath=n.parsePath=n.locationsAreEqual=n.createLocation=n.createMemoryHistory=n.createHashHistory=n.createBrowserHistory=void 0;var r=e(2);Object.defineProperty(n,"createLocation",{enumerable:!0,get:function(){return r.createLocation}}),Object.defineProperty(n,"locationsAreEqual",{enumerable:!0,get:function(){return r.locationsAreEqual}});var i=e(1);Object.defineProperty(n,"parsePath",{enumerable:!0,get:function(){return i.parsePath}}),Object.defineProperty(n,"createPath",{enumerable:!0,get:function(){return i.createPath}});var a=e(7),c=o(a),u=e(8),s=o(u),f=e(9),l=o(f);n.createBrowserHistory=c.default,n.createHashHistory=s.default,n.createMemoryHistory=l.default},function(t,n){"use strict";n.__esModule=!0;n.addLeadingSlash=function(t){return"/"===t.charAt(0)?t:"/"+t},n.stripLeadingSlash=function(t){return"/"===t.charAt(0)?t.substr(1):t},n.stripPrefix=function(t,n){return 0===t.indexOf(n)?t.substr(n.length):t},n.stripTrailingSlash=function(t){return"/"===t.charAt(t.length-1)?t.slice(0,-1):t},n.parsePath=function(t){var n=t||"/",e="",o="",r=n.indexOf("#");r!==-1&&(o=n.substr(r),n=n.substr(0,r));var i=n.indexOf("?");return i!==-1&&(e=n.substr(i),n=n.substr(0,i)),n=decodeURI(n),{pathname:n,search:"?"===e?"":e,hash:"#"===o?"":o}},n.createPath=function(t){var n=t.pathname,e=t.search,o=t.hash,r=encodeURI(n||"/");return e&&"?"!==e&&(r+="?"===e.charAt(0)?e:"?"+e),o&&"#"!==o&&(r+="#"===o.charAt(0)?o:"#"+o),r}},function(t,n,e){"use strict";function o(t){return t&&t.__esModule?t:{default:t}}n.__esModule=!0,n.locationsAreEqual=n.createLocation=void 0;var r=Object.assign||function(t){for(var n=1;n<arguments.length;n++){var e=arguments[n];for(var o in e)Object.prototype.hasOwnProperty.call(e,o)&&(t[o]=e[o])}return t},i=e(10),a=o(i),c=e(11),u=o(c),s=e(1);n.createLocation=function(t,n,e,o){var i=void 0;return"string"==typeof t?(i=(0,s.parsePath)(t),i.state=n):(i=r({},t),void 0===i.pathname&&(i.pathname=""),i.search?"?"!==i.search.charAt(0)&&(i.search="?"+i.search):i.search="",i.hash?"#"!==i.hash.charAt(0)&&(i.hash="#"+i.hash):i.hash="",void 0!==n&&void 0===i.state&&(i.state=n)),i.key=e,o&&(i.pathname?"/"!==i.pathname.charAt(0)&&(i.pathname=(0,a.default)(i.pathname,o.pathname)):i.pathname=o.pathname),i},n.locationsAreEqual=function(t,n){return t.pathname===n.pathname&&t.search===n.search&&t.hash===n.hash&&t.key===n.key&&(0,u.default)(t.state,n.state)}},function(t,n,e){"use strict";var o=function(){};t.exports=o},function(t,n,e){"use strict";function o(t){return t&&t.__esModule?t:{default:t}}n.__esModule=!0;var r=e(3),i=(o(r),function(){var t=null,n=function(n){return t=n,function(){t===n&&(t=null)}},e=function(n,e,o,r){if(null!=t){var i="function"==typeof t?t(n,e):t;"string"==typeof i?"function"==typeof o?o(i,r):r(!0):r(i!==!1)}else r(!0)},o=[],r=function(t){var n=!0,e=function(){n&&t.apply(void 0,arguments)};return o.push(e),function(){n=!1,o=o.filter(function(t){return t!==e})}},i=function(){for(var t=arguments.length,n=Array(t),e=0;e<t;e++)n[e]=arguments[e];o.forEach(function(t){return t.apply(void 0,n)})};return{setPrompt:n,confirmTransitionTo:e,appendListener:r,notifyListeners:i}});n.default=i},function(t,n){"use strict";n.__esModule=!0;n.canUseDOM=!("undefined"==typeof window||!window.document||!window.document.createElement),n.addEventListener=function(t,n,e){return t.addEventListener?t.addEventListener(n,e,!1):t.attachEvent("on"+n,e)},n.removeEventListener=function(t,n,e){return t.removeEventListener?t.removeEventListener(n,e,!1):t.detachEvent("on"+n,e)},n.getConfirmation=function(t,n){return n(window.confirm(t))},n.supportsHistory=function(){var t=window.navigator.userAgent;return(t.indexOf("Android 2.")===-1&&t.indexOf("Android 4.0")===-1||t.indexOf("Mobile Safari")===-1||t.indexOf("Chrome")!==-1||t.indexOf("Windows Phone")!==-1)&&(window.history&&"pushState"in window.history)},n.supportsPopStateOnHashChange=function(){return window.navigator.userAgent.indexOf("Trident")===-1},n.supportsGoWithoutReloadUsingHash=function(){return window.navigator.userAgent.indexOf("Firefox")===-1},n.isExtraneousPopstateEvent=function(t){return void 0===t.state&&navigator.userAgent.indexOf("CriOS")===-1}},function(t,n,e){"use strict";var o=function(t,n,e,o,r,i,a,c){if(!t){var u;if(void 0===n)u=new Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.");else{var s=[e,o,r,i,a,c],f=0;u=new Error(n.replace(/%s/g,function(){return s[f++]})),u.name="InLine truncated
|
||||
@@ -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>
|
||||
@@ -0,0 +1 @@
|
||||
/*! tailwindcss v3.3.2 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Inter var,Helvetica,sans-serif;font-feature-settings:"cv11","ss01";font-variation-settings:"opsz" 32}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--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:#3b82f680;--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: }.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}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.bottom-0{bottom:0}.left-0{left:0}.right-auto{right:auto}.top-0{top:0}.z-10{z-index:10}.z-\[48\]{z-index:48}.z-\[60\]{z-index:60}.m-4{margin:1rem}.my-0{margin-top:0;margin-bottom:0}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.mb-1{margin-bottom:.25rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.mr-2{margin-right:.5rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-12{height:3rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-full{height:100%}.max-h-64{max-height:16rem}.min-h-screen{min-height:100vh}.w-0{width:0}.w-11{width:2.75rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-72{width:18rem}.w-full{width:100%}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.flex-shrink-0,.shrink-0{flex-shrink:0}.grow-0{flex-grow:0}.basis-\[30px\]{flex-basis:30px}.-translate-x-full{--tw-translate-x:-100%}.-translate-x-full,.translate-x-0{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))}.translate-x-0{--tw-translate-x:0px}.translate-x-5{--tw-translate-x:1.25rem}.translate-x-5,.translate-y-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skeLine truncated
|
||||
@@ -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);
|
||||
@@ -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
|
||||
|
||||
@@ -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 (;;) {
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
../../modbus-dashboard/net.c
|
||||
@@ -0,0 +1 @@
|
||||
../../modbus-dashboard/net.h
|
||||
@@ -0,0 +1 @@
|
||||
../../modbus-dashboard/packed_fs.c
|
||||
Reference in new issue
Block a user