mirror of
https://github.com/cesanta/mongoose.git
synced 2024-12-12 15:39:00 +08:00
296 lines
11 KiB
C
296 lines
11 KiB
C
|
// 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);
|
||
|
}
|