-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
21 changed files
with
2,958 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../device-dashboard/certs |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../../mongoose.c |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../../mongoose.h |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,256 @@ | ||
// 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 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 = mg_htons((uint16_t) (mg_json_get_long(json, "$.reg", 1) - 1)); | ||
// uint16_t val = (uint16_t) mg_json_get_long(json, "$.val", 0); | ||
uint8_t func = (uint8_t) mg_json_get_long(json, "$.func", 0); | ||
uint16_t nregs = mg_htons((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 { | ||
uint16_t tmp = mg_htons(0x1234); | ||
// mg_random(&tmp, sizeof(tmp)); // Transaction identifier: random | ||
MG_DEBUG(("%lu TID %x, id %d, func %d, reg %d, nr %d", c->id, tmp, id, func, | ||
reg, nregs)); | ||
mg_send(c, &tmp, sizeof(tmp)); | ||
tmp = 0; | ||
mg_send(c, &tmp, sizeof(tmp)); // Protocol identifier: 0 (modbus) | ||
uint16_t *lp = (uint16_t *) &c->send.buf[c->send.len]; | ||
mg_send(c, &tmp, sizeof(tmp)); // Length: to be set later | ||
size_t len = c->send.len; | ||
|
||
mg_send(c, &id, sizeof(id)); // Client ID | ||
mg_send(c, &func, sizeof(func)); // Function | ||
|
||
mg_send(c, ®, sizeof(reg)); // Start register | ||
mg_send(c, &nregs, sizeof(nregs)); // Number of registers | ||
|
||
if (func == 16) { // Fill in register values to write | ||
uint16_t max = mg_ntohs(nregs); | ||
uint8_t nbytes = (uint8_t) (max * 2); | ||
mg_send(c, &nbytes, sizeof(nbytes)); | ||
for (uint16_t i = 0; i < max; i++) { | ||
char path[20]; | ||
mg_snprintf(path, sizeof(path), "$.values[%hu]", i); | ||
uint16_t r = mg_htons((uint16_t) mg_json_get_long(json, path, 0)); | ||
mg_send(c, &r, sizeof(r)); | ||
} | ||
} | ||
|
||
*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; | ||
} | ||
} | ||
|
||
static size_t print_regs(void (*out)(char, void *), void *ptr, va_list *ap) { | ||
size_t len = 0, num = va_arg(*ap, size_t) / 2; | ||
uint16_t *regs = va_arg(*ap, uint16_t *); | ||
for (size_t i = 0; i < num; i++) { | ||
len += mg_xprintf(out, ptr, "%s%lu", i == 0 ? "" : ",", mg_ntohs(regs[i])); | ||
} | ||
return len; | ||
} | ||
|
||
// 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; | ||
if (io->buf[7] == 16) { | ||
mg_http_reply(c, 200, s_json_header, "{%m:%s,%m:%m}\n", // | ||
MG_ESC("success"), "true", // | ||
MG_ESC("raw"), mg_print_hex, io->len - 7, io->buf + 7); | ||
} else { | ||
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_regs, io->len - 9, io->buf + 9); | ||
} | ||
} | ||
} | ||
|
||
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); | ||
} |
Oops, something went wrong.