diff --git a/test/mesh/Makefile b/test/mesh/Makefile new file mode 100644 index 00000000..01ff9347 --- /dev/null +++ b/test/mesh/Makefile @@ -0,0 +1,32 @@ +# Mesh simulator. `make` builds and runs an example +ROOT = ../.. +SRCS = simulator.c l2_mesh.c l2_802154.c star.c $(ROOT)/mongoose.c +HDRS = simulator.h l2_mesh.h l2_802154.h star.h $(ROOT)/mongoose.h +WARN = -pedantic -W -Wall -Werror -Wshadow -Wdouble-promotion -fno-common \ + -Wconversion -Wundef -Wmissing-prototypes -Wstrict-prototypes +DEFS = -DMG_ENABLE_TCPIP=1 -DMG_ENABLE_TCPIP_DRIVER_INIT=0 -DMG_ENABLE_IPV6=0 \ + -DMG_ENABLE_CUSTOM_MILLIS=1 +CFLAGS ?= -O2 -g0 $(WARN) +X ?= 100 +Y ?= 100 +ROWS ?= 10 +COLS ?= 10 + +# Nodes at every corner of a ROWS x COLS grid over X,Y, edges included +GRID = $(shell for r in $$(seq 0 $(ROWS)); do \ + for c in $$(seq 0 $(COLS)); do \ + echo -node $$((c * $(X) / $(COLS))),$$((r * $(Y) / $(ROWS))); \ + done; done) + +all: mesh_sim + ./mesh_sim -area $(X),$(Y) \ + -bridge $$(($(X) / 2)),$$(($(Y) / 2)) \ + $(GRID) + +mesh_sim: $(SRCS) $(HDRS) Makefile + $(CC) $(SRCS) $(CFLAGS) $(DEFS) -I. -I$(ROOT) -o $@ + +clean: + rm -rf mesh_sim mesh_sim.dSYM + +.PHONY: all clean diff --git a/test/mesh/README.md b/test/mesh/README.md new file mode 100644 index 00000000..991dfe70 --- /dev/null +++ b/test/mesh/README.md @@ -0,0 +1,189 @@ +# Ethernet over a radio mesh + +## The idea + +Small radio devices, e.g. IEEE 802.15.4, sub-GHz or LoRa boards, should run +Mongoose as if they were on an ordinary Ethernet LAN: get an IP address over +DHCP, answer HTTP, talk MQTT. No 6LoWPAN, no Thread, no special stack. + +To do that, the radio interface looks like Ethernet to Mongoose +(`MG_TCPIP_L2_ETH`). Its driver splits each Ethernet frame into radio +frames, sends them over the air, and the receiver puts them back together. +One or more bridges connect the radio network to the wired LAN: a bridge has +an Ethernet port and a radio. A node in range of a bridge talks to it +directly; a node out of range reaches a bridge through other nodes, which +forward its frames. That makes the radio network a mesh. With several +bridges, each node uses one of them, e.g. the nearest; the mesh covers a +bigger area, and keeps working if a bridge goes down. A PC on the LAN sees +the radio nodes as regular hosts on the same LAN. + +``` + +---- bridge 1 ~~~ node ~~~ node + | ~ + PC ---- LAN node + | ~ + +---- bridge 2 ~~~ node ~~~ node ~~~ node +``` + +`~~~` and `~` are radio hops. Nodes far from a bridge send their frames +through nearer nodes. A node in the middle can reach either bridge. + +## Two parts plug in + +- `struct mg_radio`, a radio standard: how big a radio frame is, how + addresses are written into it. `mg_radio_802154` is the first one. +- `struct mg_mesh`, a mesh strategy: how nodes find a bridge, and where each + frame goes next. `mg_mesh_star` is the first and simplest one: every node + talks to a bridge directly, one hop. + +Everything else, splitting and reassembling frames, sending again when a +frame is lost, is common code that works the same for every radio and every +mesh. The goal is to swap radios and mesh strategies without touching +anything else. + +## Why a simulator + +Real boards are slow to test with, and you can't put 500 of them on a desk. +The simulator runs a whole network in one process: every node is a real +Mongoose instance, only the radio and the air between nodes are emulated. +That lets us try mesh strategies with many nodes, and measure how long it +takes nodes to join, get an address, and answer a request. + +## Virtual time + +The simulator has its own clock, `simulator::now`, in microseconds, and +Mongoose reads it through `mg_millis()`. Nothing waits for real time: the +simulator moves the clock forward itself, 250 us per step, and polls every +station once per step. A minute of network traffic takes about a second to +simulate. `-t SEC` sets how long to run, 10 minutes by default. + +The step is a trade-off. Each step polls every station's Mongoose, and even +an idle poll costs something, so a smaller step means a slower run. A +bigger step makes frames wait longer in queues, see below. Measured with +121 nodes and 10 simulated minutes: + +| Step | Request time, mean | Run time | +|------|--------------------|----------| +| 1 ms | 6.1 ms | 0.9 s | +| 250 us | 1.5 ms | 3.0 s | +| 100 us | 0.6 ms | 7.2 s | +| 10 us | 0.06 ms | 72 s | + +## The setup + +``` + DHCP server ---------+ + 10.0.0.1 | + 02:01:00:00:00:00 +-- LAN ---- bridge ~~~ nodes + | + test station --------+ + 10.0.0.2 + 02:02:00:00:00:00 +``` + +The LAN is assumed infinitely fast, see "How frames move on the LAN" below. +Two stations are on it: + +- The DHCP server, 10.0.0.1, MAC 02:01:00:00:00:00, gives each node an + address made from its MAC: 02:00:00:X:Y:Z gets 10.X+1.Y.Z, so node + addresses never clash with the LAN stations' 10.0.0.x. +- The test station, 10.0.0.2, MAC 02:02:00:00:00:00, is a Mongoose + instance. It waits for the mesh to settle, 60 seconds by default + (`-w SEC`), then sends `GET /` to each node in turn, one at a time, at the + node's own IP address. A node with no address, or one that doesn't answer + within 10 seconds, counts as failed. At the end it prints how many + requests succeeded and how long they took. + +Bridges are on the LAN too, and get every frame, as if in promiscuous mode: +a frame for one of their radio nodes isn't addressed to the bridge itself. +A bridge forwards every frame it gets from the LAN to the radio, and every +frame from the radio to the LAN, with no filtering yet. + +Bridges and radio nodes get MACs 02:00:00:xx:xx:xx, numbered from 1 in +command-line order: 02:00:00:00:00:01, 02:00:00:00:00:02, and so on. + +### How frames move on the LAN + +Every LAN station, the DHCP server, the test station and each bridge, has a +LAN interface, `lif`: a `struct mg_tcpip_if` with a receive queue. All of +them use the same driver, `s_lan_driver`. There are no cables and no switch: + +- **Sending (tx).** A station sends a frame with its driver's `tx`, which is + `lan_tx()`. It looks at the destination MAC and copies the frame into the + receive queues of the stations it is for: + - a broadcast or multicast goes to every station but the sender; + - a unicast goes to the station with that MAC; + - bridges get every frame, as a real bridge in promiscuous mode would: + a frame for a radio node is addressed to the node, not to its bridge. + + That's all `tx` does. Nobody's code runs on the receiving side yet. +- **Receiving (rx).** Each station takes the frames from its own queue when + the main loop polls it: + - the test station runs Mongoose, which reads the queue by itself, as it + would with a real Ethernet driver that queues frames from an interrupt; + - the DHCP server reads it in `dhcp_poll()`, and replies by sending + through its own `lif`; + - bridges read it in `bridges_poll()`, and forward it to the radio. + +So a frame waits in a queue until its receiver's next poll, at most one +step of virtual time, 250 us. Ethernet is hundreds of times faster than the radio, so that +doesn't matter for what we measure. And since sending never calls the +receiver's code, a reply is never sent from inside another station's send. + +For example, a node asking for an address: + +1. The node's DHCP DISCOVER crosses the radio to a bridge, which sends it + out of its `lif`. It is a broadcast, so `lan_tx()` puts it into the + queues of the DHCP server, the test station and the other bridges. +2. On its next poll, the DHCP server reads it and sends an OFFER, also a + broadcast, which lands in the test station's and the bridges' queues. +3. On their next poll, the bridges read the OFFER and send it over the + radio to the node. The test station's Mongoose drops it: the test station + has a fixed address and no DHCP client. + +## Files + +| File | What | +|------|------| +| `l2_mesh.h`, `l2_mesh.c` | `struct mg_radio`, `struct mg_mesh`, common code | +| `l2_802154.h`, `l2_802154.c` | `mg_radio_802154`, IEEE 802.15.4 | +| `star.h`, `star.c` | `mg_mesh_star`, the star mesh | +| `simulator.h`, `simulator.c` | The simulator | +| `Makefile` | Builds and runs the simulator | + +## Where we are + +It works end to end: nodes get addresses over DHCP, and the test station's +requests reach them through the bridges. But the radio is ideal for now: + +- The radio driver works like the LAN's: it puts whole Ethernet frames + straight into receive queues, all of them for a broadcast, else the one + with the destination MAC, and bridges get every frame. No range, no + loss, no airtime. +- Bridges forward everything, with no filtering. +- So use one bridge for now. With two, a broadcast from the LAN goes to the + radio through both, each hears the other's copy and sends it back to the + LAN, and that never ends. Deciding who forwards what, without loops, is + the job of `mg_mesh`. + +`mg_radio` and `mg_mesh` are still stubs. Next steps, one at a time: the +air (range, loss, airtime), fragmenting frames into radio frames with +`mg_radio`, choosing a bridge and forwarding through nodes with `mg_mesh`, +and filtering at the bridges. + +## Running + +```sh +make # 100x100 m, a bridge, 11x11 nodes +make X=200 Y=50 ROWS=2 COLS=8 # Another area and grid +``` + +`make` puts a bridge in the middle of the area, and a node at every corner +of a `ROWS` x `COLS` grid over it. The simulator itself takes: + +``` +./mesh_sim -area X,Y,Z -bridge X,Y,Z -node X,Y,Z ... -t SEC -w SEC +``` + +Positions are in metres; `X,Y` means `Z` is 0. Repeat `-bridge` and `-node` +for more. diff --git a/test/mesh/l2_802154.c b/test/mesh/l2_802154.c new file mode 100644 index 00000000..fe872f51 --- /dev/null +++ b/test/mesh/l2_802154.c @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved + +#include "l2_802154.h" + +const struct mg_radio mg_radio_802154 = {"802.15.4"}; diff --git a/test/mesh/l2_802154.h b/test/mesh/l2_802154.h new file mode 100644 index 00000000..2012ee7d --- /dev/null +++ b/test/mesh/l2_802154.h @@ -0,0 +1,10 @@ +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved +// +// IEEE 802.15.4 radio + +#pragma once + +#include "l2_mesh.h" + +extern const struct mg_radio mg_radio_802154; diff --git a/test/mesh/l2_mesh.c b/test/mesh/l2_mesh.c new file mode 100644 index 00000000..3fd64c86 --- /dev/null +++ b/test/mesh/l2_mesh.c @@ -0,0 +1,4 @@ +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved + +#include "l2_mesh.h" diff --git a/test/mesh/l2_mesh.h b/test/mesh/l2_mesh.h new file mode 100644 index 00000000..d20f5e0a --- /dev/null +++ b/test/mesh/l2_mesh.h @@ -0,0 +1,21 @@ +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved +// +// Ethernet over radio, through a mesh. Mongoose sees an Ethernet interface, +// its driver splits Ethernet frames into radio frames. Two parts plug in: +// - struct mg_radio: a radio standard, e.g. mg_radio_802154 +// - struct mg_mesh: how the mesh is built and routed, e.g. mg_mesh_star + +#pragma once + +#include "mongoose.h" + +// A radio standard +struct mg_radio { + const char *name; // For logs +}; + +// A mesh strategy +struct mg_mesh { + const char *name; // For logs +}; diff --git a/test/mesh/simulator.c b/test/mesh/simulator.c new file mode 100644 index 00000000..927e4c3f --- /dev/null +++ b/test/mesh/simulator.c @@ -0,0 +1,433 @@ +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved +// +// Radio mesh simulator. Bridges and nodes are stations at positions in an +// area. Time is virtual: see mg_millis(). On the LAN: a DHCP server, and a +// test station that sends HTTP requests to the nodes + +#include "simulator.h" + +#define STEP_MICROSECONDS 250 // Virtual time per main loop step, see README.md + +static struct simulator s_sim; // The simulator + +// Mongoose's clock: virtual time. A minute of network traffic takes far less +// than a minute to simulate +uint64_t mg_millis(void) { + return s_sim.now / 1000; +} + +// Parse "X,Y,Z", or "X,Y" with Z 0. Return false if it is not that +static bool parse_pos(const char *s, struct pos *p) { + int n = 0; // Characters parsed + p->x = p->y = p->z = 0; + return (sscanf(s, "%lf,%lf,%lf%n", &p->x, &p->y, &p->z, &n) == 3 || + sscanf(s, "%lf,%lf%n", &p->x, &p->y, &n) == 2) && + s[n] == 0; +} + +// Is position p inside the area of sim +static bool inside(const struct simulator *sim, const struct pos *p) { + return p->x >= 0 && p->x <= sim->area.x && p->y >= 0 && p->y <= sim->area.y && + p->z >= 0 && p->z <= sim->area.z; +} + +static void usage(const struct simulator *sim, const char *prog) { + fprintf(stderr, + "Usage: %s OPTIONS\n" + "Positions: X,Y,Z in metres, or X,Y with Z 0\n" + " -area X,Y,Z - area, a box, default: %g,%g,%g\n" + " -bridge X,Y,Z - a bridge; repeat for more\n" + " -node X,Y,Z - a radio node; repeat for more\n" + " -t SEC - simulated time, seconds, default: %g\n" + " -w SEC - mesh settle time before requests, default: %g\n", + prog, sim->area.x, sim->area.y, sim->area.z, + (double) sim->duration / 1e6, (double) sim->tester.wait / 1e6); + exit(EXIT_FAILURE); +} + +// The next radio MAC, to mac: 02:00:00:xx:xx:xx, numbered from 1 +static void next_mac(struct simulator *sim, uint8_t *mac) { + uint32_t n = ++sim->last_mac; + mac[0] = 2, mac[1] = mac[2] = 0, mac[3] = (uint8_t) (n >> 16); + mac[4] = (uint8_t) (n >> 8), mac[5] = (uint8_t) n; +} + +// Add a bridge at position s. Return false if s is not a position +static bool add_bridge(struct simulator *sim, const char *s) { + struct bridge *b = (struct bridge *) calloc(1, sizeof(*b)); + if (b == NULL || !parse_pos(s, &b->pos)) return free(b), false; + next_mac(sim, b->rif.mac); + LIST_ADD_TAIL(struct bridge, &sim->bridges, b); + sim->num_bridges++; + return true; +} + +// Add a radio node at position s. Return false if s is not a position +static bool add_node(struct simulator *sim, const char *s) { + struct radio_node *n = (struct radio_node *) calloc(1, sizeof(*n)); + if (n == NULL || !parse_pos(s, &n->pos)) return free(n), false; + next_mac(sim, n->rif.mac); + LIST_ADD_TAIL(struct radio_node, &sim->nodes, n); + sim->num_nodes++; + return true; +} + +// Exit if position x is outside the area of sim +static void check_inside(const struct simulator *sim, const struct pos *x) { + if (!inside(sim, x)) { + fprintf(stderr, "%g,%g,%g is outside the area\n", x->x, x->y, x->z); + exit(EXIT_FAILURE); + } +} + +// Check that all bridges and nodes are inside the area of sim +static void check_area(const struct simulator *sim) { + struct bridge *b; + struct radio_node *n; + for (b = sim->bridges; b != NULL; b = b->next) check_inside(sim, &b->pos); + for (n = sim->nodes; n != NULL; n = n->next) check_inside(sim, &n->pos); +} + +#define LAN_QUEUE_SIZE (64 * 1024) // Receive queue of a LAN interface + +// Put frame buf, len bytes, into the receive queue of LAN interface to, if +// it is not the sender, from +static void lan_put(struct mg_tcpip_if *to, struct mg_tcpip_if *from, + const void *buf, size_t len) { + if (to != from) mg_tcpip_qwrite((void *) buf, len, to); +} + +// LAN driver, all LAN interfaces share it. The LAN is infinitely fast: tx +// puts the frame straight into the receive queues of the interfaces it is +// for: all, if broadcast; else the one with the destination MAC. Bridges get +// every frame: some are for their radio nodes. Each station takes frames +// from its queue when polled, see dhcp_poll(), bridges_poll(); Mongoose does +// it for the test station +static size_t lan_tx(const void *buf, size_t len, struct mg_tcpip_if *ifp) { + const uint8_t *dst = (const uint8_t *) buf; + bool all = dst[0] & 1; // Broadcast, multicast + struct bridge *b; + if (all || memcmp(dst, s_sim.dhcp.lif.mac, 6) == 0) { + lan_put(&s_sim.dhcp.lif, ifp, buf, len); + } + if (all || memcmp(dst, s_sim.tester.lif.mac, 6) == 0) { + lan_put(&s_sim.tester.lif, ifp, buf, len); + } + for (b = s_sim.bridges; b != NULL; b = b->next) { + lan_put(&b->lif, ifp, buf, len); + } + return len; +} + +static bool lan_init(struct mg_tcpip_if *ifp) { + (void) ifp; + return true; +} + +static bool lan_link(struct mg_tcpip_if *ifp, bool s1) { + (void) ifp, (void) s1; + return true; +} + +static struct mg_tcpip_driver s_lan_driver = {lan_init, lan_tx, NULL, lan_link}; + +// Internet checksum of len bytes +static uint16_t ip_csum(const uint8_t *buf, size_t len) { + uint32_t sum = 0; + size_t i; + for (i = 0; i + 1 < len; i += 2) sum += (uint32_t) (buf[i] << 8 | buf[i + 1]); + if (len & 1) sum += (uint32_t) (buf[len - 1] << 8); + while (sum >> 16) sum = (sum & 0xffff) + (sum >> 16); + return (uint16_t) ~sum; +} + +// A node's address, from its MAC 02:00:00:X:Y:Z: 10.X+1.Y.Z, so it never +// clashes with the LAN stations' 10.0.0.x +static void node_ip(const uint8_t *mac, uint8_t *ip) { + ip[0] = 10, ip[1] = (uint8_t) (mac[3] + 1), ip[2] = mac[4], ip[3] = mac[5]; +} + +// DHCP server, a frame from its queue: a DISCOVER gets an OFFER, a REQUEST +// an ACK. Replies are broadcast. The address comes from the client's MAC +static void dhcp_rx(struct simulator *sim, const uint8_t *buf, size_t len) { + static const uint8_t opts[] = {53, 1, 0, // Message type, set below + 1, 4, 255, 0, 0, 0, // Mask + 3, 4, 10, 0, 0, 1, // Router + 54, 4, 10, 0, 0, 1, // Server + 51, 4, 0, 0, 14, 16, // Lease, 3600 s + 255}; + uint8_t r[14 + 20 + 8 + 240 + sizeof(opts)], type = 0; + const uint8_t *dh, *p, *end = buf + len; + size_t ihl, rlen = sizeof(r); + if ((buf[0] & 1) == 0) return; // Bcast + if (len < 34 || buf[12] != 8 || buf[13] != 0 || buf[23] != 17) return; // UDP + ihl = (size_t) (buf[14] & 15) * 4, dh = buf + 14 + ihl + 8; // DHCP + if (dh + 240 > end || buf[14 + ihl + 2] != 0 || buf[14 + ihl + 3] != 67) { + return; + } + for (p = dh + 240; p + 2 <= end && p[0] != 255; p += p[0] ? p[1] + 2 : 1) { + if (p[0] == 53 && p[1] == 1 && p + 3 <= end) type = p[2]; + } + if (type != 1 && type != 3) return; // DISCOVER, REQUEST + memset(r, 0, sizeof(r)); + memset(r, 0xff, 6), memcpy(r + 6, sim->dhcp.lif.mac, 6), + r[12] = 8; // Ethernet + r[14] = 0x45, r[16] = (uint8_t) ((rlen - 14) >> 8); // IPv4 + r[17] = (uint8_t) (rlen - 14), r[22] = 64, r[23] = 17; + r[26] = 10, r[29] = 1, memset(r + 30, 0xff, 4); // 10.0.0.1 -> bcast + r[24] = (uint8_t) (ip_csum(r + 14, 20) >> 8); + r[25] = (uint8_t) ip_csum(r + 14, 20); + r[35] = 67, r[37] = 68; // UDP, csum 0: none + r[38] = (uint8_t) ((rlen - 34) >> 8), r[39] = (uint8_t) (rlen - 34); + r[42] = 2, r[43] = 1, r[44] = 6; // DHCP: reply, Ethernet + memcpy(r + 46, dh + 4, 4); // xid + memcpy(r + 52, dh + 10, 2); // flags + node_ip(dh + 28, r + 58); // yiaddr + r[62] = 10, r[65] = 1; // siaddr + memcpy(r + 70, dh + 28, 16); // chaddr + r[278] = 99, r[279] = 130, r[280] = 83, r[281] = 99; // Magic + memcpy(r + 282, opts, sizeof(opts)); + r[284] = type == 1 ? 2 : 5; // OFFER, ACK + sim->dhcp.lif.driver->tx(r, rlen, &sim->dhcp.lif); +} + +// DHCP server's work: take the frames from its queue +static void dhcp_poll(struct simulator *sim) { + char *buf; + size_t len; + while ((len = mg_queue_next(&sim->dhcp.lif.recv_queue, &buf)) > 0) { + dhcp_rx(sim, (const uint8_t *) buf, len); + mg_queue_del(&sim->dhcp.lif.recv_queue, len); + } +} + +// The request on the way finished: ok, or failed +static void tester_done(struct lan_tester *t, uint64_t now, bool ok) { + double ms = (double) (now - t->start) / 1000; + if (ok) { + t->ok++, t->sum += ms; + if (t->ok == 1 || ms < t->min) t->min = ms; + if (ms > t->max) t->max = ms; + } else { + t->failed++; + } + if (t->c != NULL) t->c->is_closing = 1, t->c->fn_data = NULL; + t->c = NULL; +} + +// HTTP client: GET /, the reply ends the request +static void tester_fn(struct mg_connection *c, int ev, void *ev_data) { + struct lan_tester *t = (struct lan_tester *) c->fn_data; + if (t == NULL) return; // Done with + if (ev == MG_EV_CONNECT) { + mg_printf(c, "GET / HTTP/1.0\r\n\r\n"); + } else if (ev == MG_EV_HTTP_MSG) { + tester_done(t, s_sim.now, true); + } else if (ev == MG_EV_ERROR || ev == MG_EV_CLOSE) { + tester_done(t, s_sim.now, false); + } + (void) ev_data; +} + +// Test station's work: Mongoose, then requests, one at a time, once the mesh +// had time to settle. A node without an IP address fails at once +static void tester_poll(struct simulator *sim) { + struct radio_node *n; + char url[64]; + mg_mgr_poll(&sim->tester.mgr, 0); + if (sim->tester.c != NULL && sim->now - sim->tester.start > + (uint64_t) sim->request_timeout_ms * 1000) { + tester_done(&sim->tester, sim->now, false); + } + if (sim->now < sim->tester.wait || sim->tester.c != NULL || + sim->tester.next == NULL) + return; + n = sim->tester.next; + sim->tester.next = n->next, sim->tester.start = sim->now; + if (n->rif.ip == 0) { + tester_done(&sim->tester, sim->now, false); + return; + } + mg_snprintf(url, sizeof(url), "http://%M/", mg_print_ip4, &n->rif.ip); + if ((sim->tester.c = mg_http_connect(&sim->tester.mgr, url, tester_fn, + &sim->tester)) == NULL) { + tester_done(&sim->tester, sim->now, false); + } +} + +// Set up sim: defaults, LAN stations +static void init(struct simulator *sim) { + static const uint8_t dmac[6] = {2, 1, 0, 0, 0, 0}, // DHCP server MAC + tmac[6] = {2, 2, 0, 0, 0, 0}; // Test station MAC + memset(sim, 0, sizeof(*sim)); + sim->area.x = sim->area.y = 100; + sim->duration = 600 * 1000000ULL, sim->tester.wait = 60 * 1000000ULL; + sim->request_timeout_ms = 10000; + memcpy(sim->dhcp.lif.mac, dmac, 6), sim->dhcp.lif.driver = &s_lan_driver; + mg_queue_init(&sim->dhcp.lif.recv_queue, (char *) calloc(1, LAN_QUEUE_SIZE), + LAN_QUEUE_SIZE); + memcpy(sim->tester.lif.mac, tmac, 6); + sim->tester.lif.ip = MG_IPV4(10, 0, 0, 2), + sim->tester.lif.mask = MG_IPV4(255, 0, 0, 0); + sim->tester.lif.driver = &s_lan_driver, + sim->tester.lif.enable_mac_check = true; + sim->tester.lif.recv_queue.size = 128 * 1024; +} + +// Radio driver, all radio interfaces share it. An ideal radio for now: +// whole Ethernet frames, no range, no loss, no airtime. It works like the +// LAN, see lan_tx(): a frame goes into the receive queues of all other +// radio interfaces if broadcast, else of the one with the destination MAC. +// Bridges get every frame: some are for the LAN. Who forwards what, and +// loops with several bridges, are for mg_mesh +static size_t radio_tx(const void *buf, size_t len, struct mg_tcpip_if *ifp) { + const uint8_t *dst = (const uint8_t *) buf; + bool all = dst[0] & 1; // Broadcast, multicast + struct bridge *b; + struct radio_node *n; + for (b = s_sim.bridges; b != NULL; b = b->next) { + lan_put(&b->rif, ifp, buf, len); + } + for (n = s_sim.nodes; n != NULL; n = n->next) { + if (all || memcmp(dst, n->rif.mac, 6) == 0) { + lan_put(&n->rif, ifp, buf, len); + } + } + return len; +} + +static bool radio_init(struct mg_tcpip_if *ifp) { + (void) ifp; + return true; +} + +static bool radio_link(struct mg_tcpip_if *ifp, bool s1) { + (void) ifp, (void) s1; + return true; +} + +static struct mg_tcpip_driver s_radio_driver = {radio_init, radio_tx, NULL, + radio_link}; + +// A node's HTTP server: the test station requests GET / +static void node_http_ev_handler(struct mg_connection *c, int ev, + void *ev_data) { + if (ev == MG_EV_HTTP_MSG) { + mg_http_reply(c, 200, "", "Hi from Mongoose, my MAC is %M\n", mg_print_mac, + c->fn_data); + } + (void) ev_data; +} + +// Start each node's Mongoose: its radio interface, no IP address, so DHCP, +// and an HTTP server +static void nodes_init(struct simulator *sim) { + struct radio_node *n; + for (n = sim->nodes; n != NULL; n = n->next) { + n->rif.driver = &s_radio_driver; + n->rif.recv_queue.size = LAN_QUEUE_SIZE; + mg_mgr_init(&n->mgr); + mg_tcpip_init(&n->mgr, &n->rif); + mg_http_listen(&n->mgr, "http://0.0.0.0:80", node_http_ev_handler, + n->rif.mac); + } +} + +// Start each bridge: its LAN and radio drivers. No Mongoose, no IP +static void bridges_init(struct simulator *sim) { + struct bridge *b; + for (b = sim->bridges; b != NULL; b = b->next) { + b->lif.driver = &s_lan_driver, b->rif.driver = &s_radio_driver; + mg_queue_init(&b->lif.recv_queue, (char *) calloc(1, LAN_QUEUE_SIZE), + LAN_QUEUE_SIZE); + mg_queue_init(&b->rif.recv_queue, (char *) calloc(1, LAN_QUEUE_SIZE), + LAN_QUEUE_SIZE); + b->lif.driver->init(&b->lif), b->rif.driver->init(&b->rif); + } +} + +// Move the frames waiting in the receive queue of interface from out of +// interface to +static void bridge_forward(struct mg_tcpip_if *from, struct mg_tcpip_if *to) { + char *buf; + size_t len; + while ((len = mg_queue_next(&from->recv_queue, &buf)) > 0) { + // MG_DEBUG(("Bridging ETH frame, size %zu", len)); + to->driver->tx(buf, len, to); + mg_queue_del(&from->recv_queue, len); + } +} + +// Bridges' work: forward every frame, LAN to radio and radio to LAN. No +// filtering yet +static void bridges_poll(struct simulator *sim) { + struct bridge *b; + for (b = sim->bridges; b != NULL; b = b->next) { + bridge_forward(&b->lif, &b->rif); + bridge_forward(&b->rif, &b->lif); + } +} + +// Poll each node's Mongoose +static void nodes_poll(struct simulator *sim) { + struct radio_node *n; + for (n = sim->nodes; n != NULL; n = n->next) mg_mgr_poll(&n->mgr, 0); +} + +int main(int argc, char *argv[]) { + clock_t started; // CPU time, to show how fast the simulation runs + int i; + + init(&s_sim); + for (i = 1; i < argc; i++) { + if (i + 1 >= argc) { + usage(&s_sim, argv[0]); + } else if (strcmp(argv[i], "-area") == 0) { + if (!parse_pos(argv[++i], &s_sim.area)) usage(&s_sim, argv[0]); + } else if (strcmp(argv[i], "-bridge") == 0) { + if (!add_bridge(&s_sim, argv[++i])) usage(&s_sim, argv[0]); + } else if (strcmp(argv[i], "-node") == 0) { + if (!add_node(&s_sim, argv[++i])) usage(&s_sim, argv[0]); + } else if (strcmp(argv[i], "-t") == 0) { + s_sim.duration = (uint64_t) (atof(argv[++i]) * 1e6); + } else if (strcmp(argv[i], "-w") == 0) { + s_sim.tester.wait = (uint64_t) (atof(argv[++i]) * 1e6); + } else { + usage(&s_sim, argv[0]); + } + } + + printf("Area %gx%gx%g m, radio %s, mesh %s, bridges: %d, nodes: %d\n", + s_sim.area.x, s_sim.area.y, s_sim.area.z, mg_radio_802154.name, + mg_mesh_star.name, s_sim.num_bridges, s_sim.num_nodes); + check_area(&s_sim); + + // mg_log_set(MG_LL_INFO); + + mg_mgr_init(&s_sim.tester.mgr); + mg_tcpip_init(&s_sim.tester.mgr, &s_sim.tester.lif); + s_sim.tester.next = s_sim.nodes; + bridges_init(&s_sim); + nodes_init(&s_sim); + + // Main loop: STEP_MICROSECONDS of virtual time per step + started = clock(); + while (s_sim.now < s_sim.duration) { + dhcp_poll(&s_sim); + tester_poll(&s_sim); + bridges_poll(&s_sim); + nodes_poll(&s_sim); + s_sim.now += STEP_MICROSECONDS; + } + printf("Requests: %d ok, %d failed, of %d nodes", s_sim.tester.ok, + s_sim.tester.failed, s_sim.num_nodes); + if (s_sim.tester.ok > 0) { + printf("; ms: min %.1f, mean %.1f, max %.1f", s_sim.tester.min, + s_sim.tester.sum / s_sim.tester.ok, s_sim.tester.max); + } + printf("\nSimulated %g s in %.3f s\n", (double) s_sim.now / 1e6, + (double) (clock() - started) / CLOCKS_PER_SEC); + + return 0; +} diff --git a/test/mesh/simulator.h b/test/mesh/simulator.h new file mode 100644 index 00000000..e4c79869 --- /dev/null +++ b/test/mesh/simulator.h @@ -0,0 +1,67 @@ +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved + +#pragma once + +#define MG_ENABLE_TCPIP 1 + +#include "l2_802154.h" +#include "l2_mesh.h" +#include "mongoose.h" +#include "star.h" + +// A position in metres +struct pos { + double x, y, z; // Coordinates +}; + +// A bridge: forwards frames between the LAN and the radio, no IP stack +struct bridge { + struct bridge *next; // Next in simulator::bridges + struct pos pos; // Where it is + struct mg_tcpip_if lif; // LAN side: LAN driver, gets every LAN frame + struct mg_tcpip_if rif; // Radio side: radio driver, gets every frame +}; + +// A radio node: Mongoose on a radio interface +struct radio_node { + struct radio_node *next; // Next in simulator::nodes + struct pos pos; // Where it is + struct mg_mgr mgr; // Mongoose event manager + struct mg_tcpip_if rif; // Radio interface. rif.ip from DHCP, 0: none yet +}; + +struct simulator; + +// LAN station: a DHCP server, 10.0.0.1. Gives each client an address made +// from its MAC, see node_ip() +struct lan_dhcp_server { + struct mg_tcpip_if lif; // Its LAN interface, no Mongoose on it +}; + +// LAN station: the test station, 10.0.0.2, a Mongoose instance. Waits for +// the mesh to settle, then sends GET / to each node +struct lan_tester { + struct mg_mgr mgr; // Its Mongoose + struct mg_tcpip_if lif; // Its LAN interface + uint64_t wait; // Mesh settle time: start requests then, us + struct radio_node *next; // Node to request next, NULL: all done + struct mg_connection *c; // Request on the way, NULL: none + uint64_t start; // When it started, us + int ok, failed; // Requests done, by result + double min, max, sum; // Times of successful requests, ms +}; + +// Simulator state +struct simulator { + struct pos area; // A box: all positions inside + struct bridge *bridges; // Bridges, a list + struct radio_node *nodes; // Radio nodes, a list + int num_bridges, num_nodes; // How many + uint64_t now; // Virtual time, us since the start + uint64_t duration; // Run this long, us + uint32_t last_mac; // Low 3 bytes of the last MAC given + int request_timeout_ms; // A request that takes longer failed + struct lan_dhcp_server dhcp; // DHCP server on the LAN + struct lan_tester tester; // Test station on the LAN +}; diff --git a/test/mesh/star.c b/test/mesh/star.c new file mode 100644 index 00000000..4fcd849f --- /dev/null +++ b/test/mesh/star.c @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved + +#include "star.h" + +const struct mg_mesh mg_mesh_star = {"star"}; diff --git a/test/mesh/star.h b/test/mesh/star.h new file mode 100644 index 00000000..3a1f28e7 --- /dev/null +++ b/test/mesh/star.h @@ -0,0 +1,10 @@ +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved +// +// Star mesh: one hop, nodes talk to a bridge + +#pragma once + +#include "l2_mesh.h" + +extern const struct mg_mesh mg_mesh_star;