Skip to content

Commit

Permalink
Add modbus dashboard
Browse files Browse the repository at this point in the history
  • Loading branch information
cpq committed Nov 27, 2023
1 parent 30738f6 commit a0b7f43
Show file tree
Hide file tree
Showing 21 changed files with 2,958 additions and 3 deletions.
55 changes: 55 additions & 0 deletions examples/modbus-dashboard/Makefile
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
26 changes: 26 additions & 0 deletions examples/modbus-dashboard/README.md
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/
1 change: 1 addition & 0 deletions examples/modbus-dashboard/certs
32 changes: 32 additions & 0 deletions examples/modbus-dashboard/main.c
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;
}
1 change: 1 addition & 0 deletions examples/modbus-dashboard/mongoose.c
1 change: 1 addition & 0 deletions examples/modbus-dashboard/mongoose.h
256 changes: 256 additions & 0 deletions examples/modbus-dashboard/net.c
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, &reg, 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);
}
Loading

0 comments on commit a0b7f43

Please sign in to comment.