Add radio simulation

This commit is contained in:
cpq committed 2026-11-05 22:23:06 +00:00
1 parent 9337a78cd0
commit d39519b92f
66 files changed
+5659 -1000

No files matched your search

+2 -2
View File
@@ -16,7 +16,7 @@ IPV6 ?= 1
MDASH ?= 1
ASAN ?= -fsanitize=address,undefined,alignment -fno-sanitize-recover=all -fno-omit-frame-pointer -fno-common
ASAN_OPTIONS ?= detect_leaks=1
TUTORIALS_EMBEDDED := $(dir $(wildcard ../tutorials/esp32/*/Makefile)) $(dir $(wildcard ../tutorials/infineon/*/Makefile)) $(dir $(wildcard ../tutorials/microchip/*/*/Makefile)) $(dir $(wildcard ../tutorials/nordic/*/Makefile)) $(dir $(wildcard ../tutorials/nxp/*/*/Makefile)) $(dir $(wildcard ../tutorials/pico-sdk/*/Makefile)) $(dir $(wildcard ../tutorials/rp/*/Makefile)) $(dir $(wildcard ../tutorials/renesas/*/Makefile)) $(dir $(wildcard ../tutorials/stm32/*/Makefile)) $(dir $(wildcard ../tutorials/stm32/*/*/Makefile)) $(dir $(wildcard ../tutorials/ti/*/Makefile)) $(dir $(wildcard ../tutorials/wch/*/Makefile))
TUTORIALS_EMBEDDED := $(dir $(wildcard ../tutorials/esp32/*/Makefile)) $(dir $(wildcard ../tutorials/infineon/*/Makefile)) $(dir $(wildcard ../tutorials/microchip/*/*/Makefile)) $(dir $(wildcard ../tutorials/nordic/*/Makefile)) $(dir $(wildcard ../tutorials/nordic/*/*/Makefile)) $(dir $(wildcard ../tutorials/nxp/*/*/Makefile)) $(dir $(wildcard ../tutorials/pico-sdk/*/Makefile)) $(dir $(wildcard ../tutorials/rp/*/Makefile)) $(dir $(wildcard ../tutorials/renesas/*/Makefile)) $(dir $(wildcard ../tutorials/silabs/*/*/Makefile)) $(dir $(wildcard ../tutorials/stm32/*/Makefile)) $(dir $(wildcard ../tutorials/stm32/*/*/Makefile)) $(dir $(wildcard ../tutorials/ti/*/Makefile)) $(dir $(wildcard ../tutorials/wch/*/Makefile))
TUTORIALS_ZEPHYR := $(dir $(wildcard ../tutorials/zephyr/*/Makefile))
TUTORIALS := ../tutorials/micropython/ $(filter-out $(TUTORIALS_EMBEDDED) $(TUTORIALS_ZEPHYR), $(dir $(wildcard ../tutorials/*/*/Makefile)))
TUTORIALS_QUICK := $(filter-out ../tutorials/micropython/ ../tutorials/tcpip/pcap-driver/, $(TUTORIALS))
@@ -98,7 +98,7 @@ mongoose.c: Makefile $(wildcard ../src/*.c) $(wildcard ../src/drivers/*.c)
cd .. && (export LC_ALL=C ; cat src/license.h; echo; echo '#include "mongoose.h"' ; (for F in src/*.c src/drivers/*.c ; do echo; echo '#ifdef MG_ENABLE_LINES'; echo "#line 1 \"$$F\""; echo '#endif'; cat $$F | sed -e 's,#include ".*,,'; done))> $@
mongoose.h: $(HDRS) Makefile
cd .. && (export LC_ALL=C ; cat src/license.h; echo; echo '#ifndef MONGOOSE_H'; echo '#define MONGOOSE_H'; echo; cat src/version.h ; echo; echo '#ifdef __cplusplus'; echo 'extern "C" {'; echo '#endif'; cat src/arch.h src/arch_*.h src/os_*.h src/net_ft.h src/net_lwip.h src/net_rl.h src/config.h src/profile.h src/str.h src/queue.h src/fmt.h src/printf.h src/log.h src/timer.h src/fs.h src/lfs.h src/util.h src/url.h src/iobuf.h src/base64.h src/md5.h src/sha1.h src/sha256.h src/event.h src/net.h src/http.h src/ssi.h src/tls.h src/tls_aes128.h src/tls_p256.h src/tls_p384.h src/tls_chacha20.h src/tls_rsa.h src/tls_mbed.h src/tls_openssl.h src/ws.h src/sntp.h src/mqtt.h src/dns.h src/modbus.h src/json.h src/jwt.h src/rpc.h src/dash.h src/ota.h src/health.h src/mdash.h src/flash.h src/wifi.h src/l2.h src/net_builtin.h src/bsd.h src/drivers/*.h | sed -e '/keep/! s,#include ".*,,' -e 's,^#pragma once,,'; echo; echo '#ifdef __cplusplus'; echo '}'; echo '#endif'; echo '#endif // MONGOOSE_H')> $@
cd .. && (export LC_ALL=C ; cat src/license.h; echo; echo '#ifndef MONGOOSE_H'; echo '#define MONGOOSE_H'; echo; cat src/version.h ; echo; echo '#ifdef __cplusplus'; echo 'extern "C" {'; echo '#endif'; cat src/arch.h src/arch_*.h src/os_*.h src/net_ft.h src/net_lwip.h src/net_rl.h src/config.h src/profile.h src/str.h src/queue.h src/fmt.h src/printf.h src/log.h src/timer.h src/fs.h src/lfs.h src/util.h src/url.h src/iobuf.h src/base64.h src/md5.h src/sha1.h src/sha256.h src/event.h src/net.h src/http.h src/ssi.h src/tls.h src/tls_aes128.h src/tls_p256.h src/tls_p384.h src/tls_chacha20.h src/tls_rsa.h src/tls_mbed.h src/tls_openssl.h src/ws.h src/sntp.h src/mqtt.h src/dns.h src/modbus.h src/json.h src/jwt.h src/rpc.h src/dash.h src/ota.h src/health.h src/mdash.h src/flash.h src/wifi.h src/l2.h src/net_builtin.h src/l2_mesh.h src/bsd.h src/drivers/*.h | sed -e '/keep/! s,#include ".*,,' -e 's,^#pragma once,,'; echo; echo '#ifdef __cplusplus'; echo '}'; echo '#endif'; echo '#endif // MONGOOSE_H')> $@
# Check that all external (exported) symbols have "mg_" prefix
mg_prefix: mongoose.c mongoose.h
+11 -8
View File
@@ -1,11 +1,14 @@
# 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
PROG ?= simulator
SRCS = simulator.c mesh_a.c $(ROOT)/mongoose.c
HDRS = simulator.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
-DMG_ENABLE_CUSTOM_MILLIS=1 -DMG_ENABLE_TCPIP_TCPRTX=1 \
-DMG_IO_SIZE=512 \
-DMG_TCPIP_ARP_MS=1000 -DMG_TCPIP_ACK_MS=500 # Slow radio: wait longer
CFLAGS ?= -O2 -g0 $(WARN)
X ?= 100
Y ?= 100
@@ -18,15 +21,15 @@ GRID = $(shell for r in $$(seq 0 $(ROWS)); do \
echo -node $$((c * $(X) / $(COLS))),$$((r * $(Y) / $(ROWS))); \
done; done)
all: mesh_sim
./mesh_sim -area $(X),$(Y) \
all: $(PROG)
$(RUN) ./$(PROG) -area $(X),$(Y) \
-bridge $$(($(X) / 2)),$$(($(Y) / 2)) \
$(GRID)
$(GRID) $(ARGS)
mesh_sim: $(SRCS) $(HDRS) Makefile
$(PROG): $(SRCS) $(HDRS) Makefile
$(CC) $(SRCS) $(CFLAGS) $(DEFS) -I. -I$(ROOT) -o $@
clean:
rm -rf mesh_sim mesh_sim.dSYM
rm -rf $(PROG) $(PROG).dSYM *.exe
.PHONY: all clean
+214 -79
View File
@@ -6,9 +6,7 @@ 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.
To do that, the radio mesh looks like an Ethernet switch to Mongoose.
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
@@ -18,28 +16,30 @@ 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
PC --- LAN --- bridge ~~~ radio nodes
```
`~~~` 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.
- `struct mg_radio` describes frame size, bitrate, airtime overhead, signal
parameters and channel-access timings.
- `struct mg_mesh` has `init`, `tick`, `free`, `tx` and `rx` callbacks. The most important
one is the `tick()` callback which is called with a microsecond timestamp,
obtained from `mg_micros()`, and handles routing and mesh management.
The mesh queues and fragments Ethernet frames and reassembles incoming radio
frames. Driver `tx()` queues Ethernet into the mesh; `rx()` returns a
reassembled Ethernet frame. A private callback transmits one radio frame.
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.
The default slotted mesh in `../../src/l2_mesh.c` uses the bridge as a coordinator.
It opens windows for new nodes to join and gives joined nodes DATA turns
in round-robin order.
Mongoose calls driver's `poll()`. Radio driver calls `mg_micros()` to get
the current timestamp and calls `mg_mesh :: tick()`.
The goal is to swap radios and mesh strategies without changing application
code. The slotted star deliberately omits radio ACKs to avoid
the extra airtime and radio turnaround. Other strategies may use them.
## Why a simulator
@@ -51,23 +51,20 @@ 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 simulator exposes its virtual clock through `mg_micros()`.
TCP/IP reads the same clock through `mg_millis()`, rounded down to milliseconds.
Nothing waits for real time. `-t SEC` sets the duration, 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:
Radio channel checks, transmission starts and ends run at their scheduled
microsecond timestamps. The clock jumps to the earliest radio event, mesh
deadline, or periodic stack poll.
| 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 |
`STEP_MICROSECONDS` is 1000: periodic polling services Mongoose's internal
stack timers every millisecond. Packet delivery triggers immediate work:
the simulator repeatedly polls DHCP, the tester, bridges and nodes at the
same virtual timestamp until that work drains. Ethernet forwarding and
radio receive processing therefore do not wait for the periodic tick.
Internal timer handling can still be delayed by up to about 1 ms.
## The setup
@@ -76,100 +73,186 @@ bigger step makes frames wait longer in queues, see below. Measured with
10.0.0.1 |
02:01:00:00:00:00 +-- LAN ---- bridge ~~~ nodes
|
test station --------+
test node --------+
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:
Two nodes 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
addresses never clash with the LAN nodes' 10.0.0.x. It also answers
ARP requests for its advertised gateway IP, 10.0.0.1, so nodes can resolve
the gateway MAC and stop retrying. It does not implement IP routing,
and does not have a full Mongoose instance
- The test node, 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.
node's own IP address. It wraps back to the first node and keeps querying
until five seconds before the end, when it stops starting new requests.
An in-flight request may still complete or time out during the remaining
time. Runs lasting five seconds or less start no requests. A node with
no address, a connection error, or a request exceeding 10 seconds counts
as failed. The summary reports successful and failed attempts, plus
minimum, mean and maximum response times for successful requests. A
request still pending at the end is not included in those counts.
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.
A bridge forwards every frame from the LAN to the radio and every frame
from the radio to the LAN.
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
Every LAN node, the DHCP server, the test node 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
- Sending. A node 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;
receive queues of the nodes it is for:
- a broadcast or multicast goes to every node but the sender;
- a unicast goes to the node 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
- Receiving. Each node 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
- the test node 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.
Enqueuing marks immediate work pending. The main loop drains that work
without advancing virtual time, so the LAN has no simulated propagation,
serialization or polling delay. Sending never calls receiver code directly;
replies are processed by subsequent polls at the same timestamp.
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.
queues of the DHCP server, the test node 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.
broadcast, which lands in the test node'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
radio to the node. The test node's Mongoose drops it: the test node
has a fixed address and no DHCP client.
### How frames move on the radio
The simulator keeps one accepted transmission per sender in a singly linked
list. A busy sender's driver returns zero, so the mesh retains the fragment
until the driver accepts it. Different senders can still overlap on air.
1. A sender waits a random number of backoff slots, then listens for
`cca_us` to check whether the channel was idle throughout that interval.
2. If busy, it increases its backoff exponent up to `backoff_max`, waits
again, and repeats. There is no channel-access attempt limit yet.
3. If idle, it schedules transmission after `turnaround_us`. Another sender
can also pass its check before either starts: the channel check does not
reserve the air.
4. Transmission takes `(len + overhead) * 8 / bitrate` seconds, rounded up
to a microsecond. Overlapping transmissions are both marked collided.
Channel checks at the same timestamp run before transmission starts,
so list order cannot make one simultaneous check win.
5. At the end, a collided frame is dropped. Otherwise it is queued for all
other nodes for broadcast/multicast, or the matching node for unicast.
Every other bridge receives it too. Receive processing happens at the
same virtual timestamp.
A receiver drops a frame if any part of that frame overlaps its own channel
backoff, sensing, turnaround or transmission interval, even if it is
available again when the frame ends.
The radio summary reports total sent and collided frames, and lost receptions.
Losses include range or random loss, plus missed unicast receptions when the
destination was transmitting.
The 802.15.4 definition uses 250 kbit/s, 8 bytes overhead, 320 us backoff
slots, 128 us channel sensing and 100 us turnaround. The initial backoff
exponent is 3 (0–7 slots), increasing to 5 (0–31 slots) on busy checks.
After transmitting, a sender waits the radio's inter-frame spacing,
`mg_radio::ifs_us`, 640 us for 802.15.4, before starting its next frame's
backoff. This wait applies even if the frame collided or the sender's
queue was temporarily empty. Reception remains enabled during the
inter-frame wait.
The backoff generator is seeded from the wall clock.
The mesh limits each transmission to 125 bytes plus the 2-byte FCS.
Overhead and inter-frame spacing apply to each fragment.
A receiver must be within `-range METRES` of the sender (default 185 m).
`-loss PERCENT` adds independent random loss for each in-range reception
(default 0%). The radio summary counts range and random losses together.
Collisions and channel sensing still apply across the whole simulated air.
## 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 |
| `../../src/l2_mesh.h`, `../../src/l2_mesh.c` | `struct mg_radio`, `struct mg_mesh`, shared node initialization, and IEEE 802.15.4 radio |
| `mesh_a.c` | Alternative unslotted star strategy |
| `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 slotted strategy uses direct radio hops between nodes and a bridge.
The simulator models airtime, half-duplex reception and collisions.
- 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`.
- Frames outside reception range are not received, so nodes out of a bridge's
range never join. But collisions and channel sensing are still global:
every overlap is a collision, wherever the receiver is, and a busy
channel is busy for everyone.
- Mesh routing remains unimplemented: bridge selection, forwarding through other
nodes and recovery are not implemented.
`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.
Next steps include distance-aware collision and channel sensing, mesh
routing and bridge filtering.
## Strategy A — unslotted (simulator default)
Each sender contends for the channel using CSMA. The mesh queues radio
fragments and submits them one at a time. The receiver reassembles bursts
per sender, and the bridge forwards Ethernet frames between LAN and radio.
## Default strategy — slotted mesh
One bridge coordinates the network and starts with an empty member list.
It broadcasts a JOIN beacon to open a 3 ms window. Unjoined nodes choose
windows at random and add up to 640 µs of jitter before sending their JOIN
request with their Ethernet MAC to the bridge. The bridge adds each new
radio address and its Ethernet MAC to the head of its member list.
After the JOIN window, the bridge grants each member a DATA turn in list
order, then opens another JOIN window. The bridge advances as soon as it
reassembles a valid uplink. If the reply is missing, the turn times out
after allowing for the downlink and a maximum-size uplink burst, including
inter-frame spacing. A member must answer every turn,
with an empty `DATA_UP` if it has nothing to send. A member that misses 3
turns in a row is removed. With no
members, the bridge opens JOIN windows back to back. A node recognizes its
first `DATA_DOWN` as
confirmation that it joined.
Slotted nodes have a separate TCP/IP interface that reports link up after
the first grant. Each mesh interface has one 2 KiB TX queue of split
radio bursts. The bridge shares that queue among members and sends the
head burst in its destination's turn. It directs DHCP replies by the
client MAC in `chaddr` and keeps one copy of other multicast and broadcast
bursts until each member has had its turn. A frame that does not fit is
rejected in full. The hardware and simulator drivers accept one raw frame
at a time. JOIN messages fit in a single radio frame. A DATA
burst carries one Ethernet frame across up to 14 radio frames. Each
fragment has a 9-byte short-address MAC header, a one-byte type, and a
byte containing the fragment index and final index in the high and low
nibbles. Empty DATA messages use a single header-only frame. The node
starts its uplink after receiving the whole downlink, and the bridge
forwards only complete uplinks to the LAN.
## Running
@@ -182,8 +265,60 @@ make X=200 Y=50 ROWS=2 COLS=8 # Another area and grid
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
./mesh_sim -area X,Y,Z -bridge X,Y,Z -node X,Y,Z ... -t SEC -w SEC -m A|default -range METRES -loss PERCENT
```
Positions are in metres; `X,Y` means `Z` is 0. Repeat `-bridge` and `-node`
for more.
## Hardware
The FRDM-RW612 example is the Ethernet bridge. The XIAO MG24 and
FRDM-MCXW71 examples are radio clients. All three use the slotted mesh.
```sh
make -C tutorials/nxp/frdm-rw612/bridge
make -C tutorials/silabs/xiao-mg24/minimal
make -C tutorials/nxp/frdm-mcxw71/minimal
```
Use each tutorial's `flash` target. Connect the
bridge to an Ethernet LAN with a DHCP server. Find the client's assigned
IP in its serial log, then send an HTTP GET to that IP from the LAN.
All three boards use channel 15 and PAN `0xabcd` by default. Each board derives its radio address from its Ethernet MAC.
MG24 is a node only; its radio address is derived from its MAC.
The drivers in `src/drivers` send and receive raw 802.15.4 frames.
They use the same slotted mesh implementation as the simulator.
The board driver is initialized by `mg_mgr_init()`. The RW612 bridge
has its own `bridge/main.c`, which initializes and polls the two drivers
directly, without an event manager or local IP stack.
## Hardware integration
The default mesh and radio drivers are included in `mongoose.c` and `mongoose.h`.
Radio drivers live in `src/drivers/`: `mg24`, `mcxw71`, `rw612w`,
and `nrf52`. MG24 driver data contains its mesh state and `strategy`; it has
no bridge LAN interface.
A NULL strategy selects `mg_mesh_default`, named `"default"`; assigning another
`const struct mg_mesh *` overrides it. Hardware callbacks transmit and receive
raw radio frames; the selected strategy owns joining, scheduling and Ethernet
fragmentation. Driver initialization goes through `mg_tcpip_init()`.
Board examples are under `tutorials/nxp/frdm-rw612/bridge`,
`tutorials/nxp/frdm-mcxw71/minimal`, `tutorials/silabs/xiao-mg24/minimal`, and
`tutorials/nordic/xiao-52840/minimal`. Their HALs supply `mg_micros()`.
The simulator and all radio drivers keep the hardware driver installed throughout the
interface lifetime. Mesh state belongs to `driver_data`. Driver `tx()` queues
Ethernet frames; `poll()` feeds raw received frames to the mesh and calls
strategy `tick()`. The strategy sends scheduled fragments through the driver's
private `radio_tx` callback. Driver `rx()` returns a reassembled Ethernet frame
using the stack's existing receive buffer. There is no mesh polling timer,
copied radio interface, or mesh-specific change in `net_builtin.*`.
The simulator explicitly frees its mesh state after `mg_mgr_free()`; board
drivers keep their state for the firmware lifetime. MG24, MCXW71 and nRF52 are
radio nodes. The RW612 bridge application owns Ethernet initialization, its RX
queue, promiscuous mode and link polling, and attaches the LAN to the mesh.
The RW612W driver owns only the radio interface.
-6
View File
@@ -1,6 +0,0 @@
// Copyright (c) 2026 Cesanta Software Limited
// All rights reserved
#include "l2_802154.h"
const struct mg_radio mg_radio_802154 = {"802.15.4"};
-10
View File
@@ -1,10 +0,0 @@
// 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;
-4
View File
@@ -1,4 +0,0 @@
// Copyright (c) 2026 Cesanta Software Limited
// All rights reserved
#include "l2_mesh.h"
-21
View File
@@ -1,21 +0,0 @@
// 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
};
+261
View File
@@ -0,0 +1,261 @@
// Copyright (c) 2026 Cesanta Software Limited
// All rights reserved
#include "mongoose.h"
// Radio header followed by type and fragment index/last index.
#define MGI_FRAME_HDR_LEN (MG_802154_HEADER_LEN + 2)
#define MGI_ETH_MAX 1514
#define MGI_TX_DST_LEN 2
#define MGI_RX_TIMEOUT_US 1000000
#define MGI_TX_QUEUE_SIZE 4096
#define MGI_DATA_TAG \
0x80u // High bit identifies DATA, low seven bits tag a burst
struct mgi_mesh_a_rx {
struct mgi_mesh_a_rx *next;
uint64_t expires;
uint16_t src;
uint8_t tag, index, last;
size_t len;
uint8_t buf[MGI_ETH_MAX];
};
struct mgi_mesh_a_data {
struct mg_mesh_node *node;
struct mg_tcpip_if *ethernet;
struct mgi_mesh_a_rx *rx;
uint8_t tag;
struct mg_queue tx;
char tx_buf[MGI_TX_QUEUE_SIZE];
size_t tx_offset;
uint64_t tx_at;
};
static const struct mg_radio *const s_radio = &mg_radio_802154;
static uint64_t mgi_mesh_a_airtime(size_t len) {
const struct mg_radio *radio = &mg_radio_802154;
return ((uint64_t) (len + radio->overhead) * 8000000 + radio->bitrate - 1) /
radio->bitrate;
}
// Each queue record holds a destination and a complete burst.
static size_t mgi_mesh_a_queue(struct mg_mesh_node *node, struct mg_queue *q,
const void *buf, size_t len, uint16_t dst, uint8_t type) {
const struct mg_radio *radio = &mg_radio_802154;
const uint8_t *eth = (const uint8_t *) buf;
size_t hdr = MGI_FRAME_HDR_LEN, cap = radio->max_frame_size - hdr;
size_t frames, size, i;
char *record;
uint8_t *out;
if (len < 14 || len > MGI_ETH_MAX) return 0;
frames = (len + cap - 1) / cap;
size = MGI_TX_DST_LEN + len + frames * (1 + hdr);
if (mg_queue_next(q, NULL) == 0) mg_queue_init(q, q->buf, q->size);
if (mg_queue_book(q, &record, size) < size) return 0;
out = (uint8_t *) record;
*out++ = (uint8_t) (dst >> 8);
*out++ = (uint8_t) dst;
for (i = 0; i < frames; i++) {
size_t chunk = len - i * cap;
if (chunk > cap) chunk = cap;
*out++ = (uint8_t) (hdr + chunk);
radio->print_hdr(radio, out, dst, node->radio_address);
out[radio->header_len] = type;
out[radio->header_len + 1] = (uint8_t) (i << 4 | (frames - 1));
memcpy(out + hdr, eth + i * cap, chunk);
out += hdr + chunk;
}
mg_queue_add(q, size);
return len;
}
static size_t mgi_mesh_a_send(struct mgi_mesh_a_data *data, const void *buf,
size_t len) {
uint8_t tag = (uint8_t) ((data->tag + 1) & 0x7f);
size_t queued = mgi_mesh_a_queue(data->node, &data->tx, buf, len, 0xffffu,
(uint8_t) (MGI_DATA_TAG | tag));
if (queued != 0) data->tag = tag;
return queued;
}
static void mgi_mesh_a_tx(struct mgi_mesh_a_data *data, uint64_t now) {
struct mg_tcpip_if *radio = data->node->radio;
char *record;
size_t size = mg_queue_next(&data->tx, &record);
if (size == 0) return;
if (now >= data->tx_at) {
const uint8_t *part = (const uint8_t *) record + data->tx_offset;
size_t len = part[0];
if (now >= data->node->tx_at &&
data->node->tx(radio, part + 1, len) == len) {
data->tx_offset += len + 1;
if (data->tx_offset == size) {
mg_queue_del(&data->tx, size);
data->tx_offset = MGI_TX_DST_LEN;
}
data->tx_at = now + mgi_mesh_a_airtime(len) + s_radio->ifs_us;
} else {
data->tx_at = now + s_radio->slot_us;
}
}
if (mg_queue_next(&data->tx, NULL) != 0) data->node->next_tick = data->tx_at;
}
static size_t mgi_mesh_a_enqueue(struct mg_mesh_node *node, const void *buf,
size_t len) {
return mgi_mesh_a_send(node->data, buf, len);
}
static void mgi_mesh_a_deliver(struct mgi_mesh_a_data *data, uint8_t *buf,
size_t len) {
struct mg_tcpip_if *lan = data->node->lan;
if (len < 14) return;
if (lan != NULL) {
lan->driver->tx(buf, len, lan);
} else {
struct mg_tcpip_if *ifp = data->node->radio;
if (data->node->rx_len != 0 || len > ifp->recv_queue.size) {
ifp->ndrop++;
} else {
memcpy(ifp->recv_queue.buf, buf, len);
data->node->rx_len = len;
}
}
}
static void mgi_mesh_a_rx_free(struct mgi_mesh_a_data *data,
struct mgi_mesh_a_rx **p) {
struct mgi_mesh_a_rx *r = *p;
*p = r->next;
data->node->ram_used -= sizeof(*r);
free(r);
}
static void mgi_mesh_a_receive(struct mgi_mesh_a_data *data, uint8_t *frame,
size_t len, uint64_t now) {
struct mgi_mesh_a_rx **p = &data->rx, *r;
size_t cap = s_radio->max_frame_size - MGI_FRAME_HDR_LEN, chunk;
uint16_t dst, src;
uint8_t tag, index, last;
if (len <= MGI_FRAME_HDR_LEN || len > s_radio->max_frame_size ||
s_radio->parse_hdr(s_radio, frame, len, &dst, &src) !=
s_radio->header_len ||
(dst != 0xffffu && dst != data->node->radio_address))
return;
tag = frame[MGI_FRAME_HDR_LEN - 2];
index = frame[MGI_FRAME_HDR_LEN - 1] >> 4;
last = frame[MGI_FRAME_HDR_LEN - 1] & 15;
chunk = len - MGI_FRAME_HDR_LEN;
if ((tag & MGI_DATA_TAG) == 0 || index > last ||
last > (MGI_ETH_MAX - 1) / cap || (index < last && chunk != cap))
return;
if (index == 0 &&
(chunk < 14 ||
(data->node->lan == NULL && (frame[MGI_FRAME_HDR_LEN] & 1) == 0 &&
memcmp(frame + MGI_FRAME_HDR_LEN, data->ethernet->mac, 6) != 0)))
return;
while (*p != NULL && (*p)->src != src) p = &(*p)->next;
if (index == 0) {
if (*p != NULL) mgi_mesh_a_rx_free(data, p);
if (last == 0) {
mgi_mesh_a_deliver(data, frame + MGI_FRAME_HDR_LEN, chunk);
return;
}
r = calloc(1, sizeof(*r));
if (r == NULL) return;
data->node->ram_used += sizeof(*r);
if (data->node->ram_peak < data->node->ram_used)
data->node->ram_peak = data->node->ram_used;
r->next = *p;
*p = r;
r->src = src;
r->tag = tag;
r->last = last;
}
r = *p;
if (r == NULL || r->tag != tag) return;
if (r->index != index || r->last != last || chunk > sizeof(r->buf) - r->len) {
mgi_mesh_a_rx_free(data, p);
return;
}
memcpy(r->buf + r->len, frame + MGI_FRAME_HDR_LEN, chunk);
r->len += chunk;
r->index++;
r->expires = now + MGI_RX_TIMEOUT_US;
if (index == last) {
mgi_mesh_a_deliver(data, r->buf, r->len);
mgi_mesh_a_rx_free(data, p);
}
}
static void mgi_mesh_a_input(struct mg_mesh_node *node, void *buf, size_t len) {
uint64_t ready = node->now + node->strategy->radio->turnaround_us;
if (node->tx_at < ready) node->tx_at = ready;
node->next_tick = node->now;
mgi_mesh_a_receive(node->data, buf, len, node->now);
}
static void mgi_mesh_a_tick(struct mg_mesh_node *node, uint64_t now) {
struct mgi_mesh_a_data *data = node->data;
struct mgi_mesh_a_rx **p = &data->rx;
size_t len;
node->now = now;
node->next_tick = (uint64_t) -1;
while (*p != NULL) {
if ((*p)->expires <= now)
mgi_mesh_a_rx_free(data, p);
else
p = &(*p)->next;
}
if (node->lan != NULL) {
struct mg_queue *q = &node->lan->recv_queue;
char *buf;
while ((len = mg_queue_next(q, &buf)) > 0) {
mgi_mesh_a_send(data, buf, len);
mg_queue_del(q, len);
}
}
mgi_mesh_a_tx(data, now);
for (p = &data->rx; *p != NULL; p = &(*p)->next) {
if ((*p)->expires < node->next_tick) node->next_tick = (*p)->expires;
}
}
static void mgi_mesh_a_free(struct mg_mesh_node *node) {
struct mgi_mesh_a_data *data = node->data;
if (data == NULL) return;
while (data->rx != NULL) mgi_mesh_a_rx_free(data, &data->rx);
free(data);
node->data = NULL;
}
static bool mgi_mesh_a_init(struct mg_mesh_node *node,
struct mg_tcpip_if *radio, struct mg_tcpip_if *lan,
uint16_t radio_address) {
struct mgi_mesh_a_data *data = calloc(1, sizeof(*data));
memset(node, 0, sizeof(*node));
node->radio = radio;
node->lan = lan;
node->radio_address = radio_address;
node->next_tick = (uint64_t) -1;
node->ram_used = node->ram_peak = sizeof(*node);
if (data == NULL) return false;
node->ram_used += sizeof(*data);
node->ram_peak = node->ram_used;
data->node = node;
data->tx_offset = MGI_TX_DST_LEN;
node->data = data;
mg_queue_init(&data->tx, data->tx_buf, sizeof(data->tx_buf));
data->ethernet = radio;
node->joined = lan == NULL;
return true;
}
const struct mg_mesh mg_mesh_a = {
"A", mgi_mesh_a_init, mgi_mesh_a_tick, mgi_mesh_a_free,
mgi_mesh_a_enqueue, mgi_mesh_a_input, false, false, &mg_radio_802154,
};
+437 -94
View File
@@ -1,20 +1,25 @@
// 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
// Radio mesh simulator. Bridges and radio nodes have positions in an area. Time
// is virtual: see mg_millis(). On the LAN: a DHCP server, and a test node that
// sends HTTP requests to the nodes
#include "simulator.h"
#define STEP_MICROSECONDS 250 // Virtual time per main loop step, see README.md
#define STEP_MICROSECONDS 1000 // Periodic stack timer polling only
static const struct mg_mesh *s_mesh = &mg_mesh_default;
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_micros(void) {
return s_sim.now;
}
uint64_t mg_millis(void) {
return s_sim.now / 1000;
return mg_micros() / 1000;
}
// Parse "X,Y,Z", or "X,Y" with Z 0. Return false if it is not that
@@ -40,9 +45,14 @@ static void usage(const struct simulator *sim, const char *prog) {
" -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",
" -w SEC - mesh settle time before requests, default: %g\n"
" -range METRES - radio range, default: %g\n"
" -loss PERCENT - random reception loss, default: %g\n"
" -m A|default - mesh strategy, default: default\n",
prog, sim->area.x, sim->area.y, sim->area.z,
(double) sim->duration / 1e6, (double) sim->tester.wait / 1e6);
(double) (int64_t) sim->duration / 1e6,
(double) (int64_t) sim->tester.wait / 1e6, sim->radio_range,
sim->radio_loss);
exit(EXIT_FAILURE);
}
@@ -57,7 +67,9 @@ static void next_mac(struct simulator *sim, uint8_t *mac) {
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;
if (sim->last_mac >= 0xffffu - 1u) return free(b), false;
next_mac(sim, b->rif.mac);
b->radio.address = (uint16_t) sim->last_mac;
LIST_ADD_TAIL(struct bridge, &sim->bridges, b);
sim->num_bridges++;
return true;
@@ -67,7 +79,9 @@ static bool add_bridge(struct simulator *sim, const char *s) {
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;
if (sim->last_mac >= 0xffffu - 1u) return free(n), false;
next_mac(sim, n->rif.mac);
n->radio.address = (uint16_t) sim->last_mac;
LIST_ADD_TAIL(struct radio_node, &sim->nodes, n);
sim->num_nodes++;
return true;
@@ -95,15 +109,18 @@ static void check_area(const struct simulator *sim) {
// 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);
if (to != from) {
mg_tcpip_qwrite((void *) buf, len, to);
s_sim.work_pending = true;
}
}
// 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
// every frame: some are for their radio nodes. Each node takes frames
// from its queue when polled, see dhcp_poll() and the mesh bridge callback;
// Mongoose does it for the test node
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
@@ -143,7 +160,7 @@ static uint16_t ip_csum(const uint8_t *buf, size_t len) {
}
// 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
// clashes with the LAN nodes' 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];
}
@@ -192,19 +209,42 @@ static void dhcp_rx(struct simulator *sim, const uint8_t *buf, size_t len) {
sim->dhcp.lif.driver->tx(r, rlen, &sim->dhcp.lif);
}
// The DHCP node also owns the advertised gateway IP, 10.0.0.1.
static bool dhcp_arp(struct simulator *sim, const uint8_t *f, size_t n) {
static const uint8_t header[] = {0, 1, 8, 0, 6, 4, 0, 1};
static const uint8_t ip[] = {10, 0, 0, 1};
uint8_t reply[60];
if (n < 42 || f[12] != 8 || f[13] != 6 ||
memcmp(f + 14, header, sizeof(header)) != 0 ||
memcmp(f + 38, ip, sizeof(ip)) != 0)
return false;
memset(reply, 0, sizeof(reply));
memcpy(reply, f + 6, 6);
memcpy(reply + 6, sim->dhcp.lif.mac, 6);
memcpy(reply + 12, f + 12, 8);
reply[21] = 2;
memcpy(reply + 22, sim->dhcp.lif.mac, 6);
memcpy(reply + 28, ip, sizeof(ip));
memcpy(reply + 32, f + 22, 10);
sim->dhcp.lif.driver->tx(reply, sizeof(reply), &sim->dhcp.lif);
return true;
}
// 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);
if (!dhcp_arp(sim, (const uint8_t *) buf, len)) {
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;
double ms = (double) (int64_t) (now - t->start) / 1000;
if (ok) {
t->ok++, t->sum += ms;
if (t->ok == 1 || ms < t->min) t->min = ms;
@@ -214,6 +254,7 @@ static void tester_done(struct lan_tester *t, uint64_t now, bool ok) {
}
if (t->c != NULL) t->c->is_closing = 1, t->c->fn_data = NULL;
t->c = NULL;
s_sim.work_pending = true;
}
// HTTP client: GET /, the reply ends the request
@@ -230,8 +271,8 @@ static void tester_fn(struct mg_connection *c, int ev, void *ev_data) {
(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
// Test node's work: Mongoose, then requests, one at a time, once the mesh
// had time to settle. Cycle through nodes until five seconds before the end.
static void tester_poll(struct simulator *sim) {
struct radio_node *n;
char url[64];
@@ -241,28 +282,34 @@ static void tester_poll(struct simulator *sim) {
tester_done(&sim->tester, sim->now, false);
}
if (sim->now < sim->tester.wait || sim->tester.c != NULL ||
sim->tester.next == NULL)
sim->tester.next == NULL || sim->duration <= 5000000U ||
sim->now >= sim->duration - 5000000U)
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);
sim->tester.next = n->next == NULL ? sim->nodes : n->next;
if (n->mgr.ifp == NULL) return;
sim->tester.start = sim->now;
if (n->mgr.ifp->ip == 0) {
// Wait for the next poll rather than endlessly cycling at this time.
sim->tester.failed++;
return;
}
mg_snprintf(url, sizeof(url), "http://%M/", mg_print_ip4, &n->rif.ip);
mg_snprintf(url, sizeof(url), "http://%M/", mg_print_ip4, &n->mgr.ifp->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
// Set up sim: defaults, LAN nodes
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
tmac[6] = {2, 2, 0, 0, 0, 0}; // Test node MAC
memset(sim, 0, sizeof(*sim));
sim->area.x = sim->area.y = 100;
sim->duration = 600 * 1000000ULL, sim->tester.wait = 60 * 1000000ULL;
sim->radio_range = 185;
sim->loss_random = 1;
sim->duration = 600 * 1000000U, sim->tester.wait = 60 * 1000000U;
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),
@@ -275,42 +322,258 @@ static void init(struct simulator *sim) {
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);
// The driver queues each submitted frame for transmission over the air.
// All nodes hear one channel. CCA and turnaround are separate events:
// two idle checks can succeed before either node starts transmitting.
// Inter-frame spacing is per sender, not a reservation of the shared air.
uint64_t radio_airtime(size_t len) {
const struct mg_radio *radio = s_mesh->radio;
return ((uint64_t) (len + radio->overhead) * 8000000 + radio->bitrate - 1) /
radio->bitrate;
}
static void radio_schedule(struct simulator *sim, struct radio_frame *f,
bool busy) {
struct radio_port *port = (struct radio_port *) f->from->driver_data;
const struct mg_radio *radio = s_mesh->radio;
bool checked = f->state == RADIO_CCA;
if (s_mesh->scheduled_tx || !radio->csma || (checked && !busy)) {
f->state = RADIO_START;
f->due = sim->now + (checked ? radio->turnaround_us : 0);
return;
}
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);
if (!checked) {
f->backoff = radio->backoff_min;
} else if (f->backoff < radio->backoff_max) {
f->backoff++;
}
if (port->random == 0) {
size_t i;
port->random = (uint32_t) mg_now();
for (i = 0; i < 6; i++) {
port->random = port->random * 33u + f->from->mac[i];
}
if (port->random == 0) port->random = 1;
}
port->random ^= port->random << 13;
port->random ^= port->random >> 17;
port->random ^= port->random << 5;
f->state = RADIO_BACKOFF;
f->due = sim->now + (port->random % (1U << f->backoff)) * radio->slot_us;
if (f->due > sim->now) port->rx_off_until = f->due;
}
static size_t radio_send(struct mg_tcpip_if *ifp, const void *buf, size_t len) {
struct radio_port *port = (struct radio_port *) ifp->driver_data;
struct simulator *sim = port->sim;
struct radio_frame *f, **p = &sim->air_head;
const struct mg_radio *radio = s_mesh->radio;
if (len < radio->header_len || len > sizeof(f->buf) ||
len > radio->max_frame_size || port->tx_ready_at > sim->now ||
radio->bitrate == 0)
return 0;
while (*p != NULL) {
if ((*p)->from == ifp) return 0;
p = &(*p)->next;
}
if ((f = (struct radio_frame *) calloc(1, sizeof(*f))) == NULL) return 0;
f->from = ifp, f->len = len;
memcpy(f->buf, buf, len);
*p = f;
sim->work_pending = true;
radio_schedule(sim, f, false);
return len;
}
static void radio_receive(struct radio_port *port,
const struct radio_frame *f) {
char *buf;
if (mg_queue_book(&port->rx, &buf, f->len) >= f->len) {
memcpy(buf, f->buf, f->len);
mg_queue_add(&port->rx, f->len);
port->sim->work_pending = true;
}
}
// Test range and sample independent interference loss for this reception.
static bool visible(struct simulator *sim, const struct pos *a,
const struct pos *b) {
double dx = a->x - b->x, dy = a->y - b->y, dz = a->z - b->z;
double range = sim->radio_range;
if (dx * dx + dy * dy + dz * dz > range * range) return false;
if (sim->radio_loss == 0) return true;
sim->loss_random ^= sim->loss_random << 13;
sim->loss_random ^= sim->loss_random >> 17;
sim->loss_random ^= sim->loss_random << 5;
return (double) sim->loss_random * (100.0 / 4294967296.0) >= sim->radio_loss;
}
static void radio_deliver_to(struct radio_port *port,
const struct radio_port *from,
const struct radio_frame *f, uint16_t dst) {
bool all = dst == 0xffffu;
if (port == from || (!all && port->address != dst)) return;
if (!visible(port->sim, from->pos, port->pos)) {
port->sim->air_lost++;
} else if (port->tx_until > f->start) {
if (!all) port->sim->air_lost++;
} else if (!f->collided && port->rx_off_until <= f->start) {
radio_receive(port, f);
}
}
static void radio_deliver(struct simulator *sim, const struct radio_frame *f) {
const struct mg_radio *radio = s_mesh->radio;
const struct radio_port *from = f->from->driver_data;
uint16_t dst, src;
struct bridge *b;
struct radio_node *n;
if (radio->parse_hdr(radio, f->buf, f->len, &dst, &src) == 0) return;
for (b = sim->bridges; b != NULL; b = b->next)
radio_deliver_to(&b->radio, from, f, dst);
for (n = sim->nodes; n != NULL; n = n->next)
radio_deliver_to(&n->radio, from, f, dst);
}
// Process ends, then all channel checks, then starts at this timestamp.
// Checks therefore see the same air state regardless of list order.
static void radio_poll(struct simulator *sim) {
struct radio_frame **p = &sim->air_head, *f, *g;
while ((f = *p) != NULL) {
if (f->state != RADIO_END || f->due > sim->now) {
p = &f->next;
continue;
}
if (f->collided) {
sim->air_collisions++;
}
radio_deliver(sim, f);
*p = f->next;
free(f);
}
for (f = sim->air_head; f != NULL; f = f->next) {
if (f->state == RADIO_BACKOFF && f->due <= sim->now) {
f->state = RADIO_CCA;
f->due = sim->now + s_mesh->radio->cca_us;
((struct radio_port *) f->from->driver_data)->rx_off_until = f->due;
}
}
for (f = sim->air_head; f != NULL; f = f->next) {
if (f->state != RADIO_CCA || f->due > sim->now) continue;
radio_schedule(sim, f,
sim->air_busy_until > sim->now - s_mesh->radio->cca_us);
if (f->state == RADIO_START) {
((struct radio_port *) f->from->driver_data)->rx_off_until = f->due;
}
}
for (f = sim->air_head; f != NULL; f = f->next) {
struct radio_port *port = f->from->driver_data;
if (f->state != RADIO_START || f->due > sim->now) continue;
f->state = RADIO_END;
f->start = sim->now;
f->due = sim->now + radio_airtime(f->len);
port->rx_off_until = port->tx_until = f->due;
port->tx_ready_at = f->due + s_mesh->radio->ifs_us;
for (g = sim->air_head; g != NULL; g = g->next) {
if (g != f && g->state == RADIO_END && g->due > sim->now) {
f->collided = g->collided = true;
}
}
if (f->due > sim->air_busy_until) sim->air_busy_until = f->due;
sim->air_sent++;
}
}
// Radio events keep microsecond precision between periodic timer polls.
static uint64_t radio_next(const struct simulator *sim, uint64_t next) {
const struct radio_frame *f;
for (f = sim->air_head; f != NULL; f = f->next) {
if (f->due < next) next = f->due;
}
return next;
}
static size_t radio_receive_frame(void *buf, size_t cap, struct mg_tcpip_if *ifp) {
struct radio_port *port = (struct radio_port *) ifp->driver_data;
char *src;
size_t len;
while ((len = mg_queue_next(&port->rx, &src)) > 0) {
if (len <= cap) memcpy(buf, src, len);
mg_queue_del(&port->rx, len);
if (len <= cap) {
if (mg_queue_next(&port->rx, &src) > 0) port->sim->work_pending = true;
return len;
}
}
return 0;
}
static size_t radio_tx(const void *buf, size_t len, struct mg_tcpip_if *ifp) {
struct radio_port *port = ifp->driver_data;
return port->mesh->strategy->tx(port->mesh, buf, len);
}
static size_t radio_rx(void *buf, size_t cap, struct mg_tcpip_if *ifp) {
struct radio_port *port = ifp->driver_data;
size_t len = port->mesh->rx_len;
if (len > cap) return 0;
if (len != 0 && buf != ifp->recv_queue.buf)
memcpy(buf, ifp->recv_queue.buf, len);
port->mesh->rx_len = 0;
return len;
}
static bool radio_driver_poll(struct mg_tcpip_if *ifp, bool s1) {
struct radio_port *port = ifp->driver_data;
uint8_t frame[128];
size_t len;
(void) s1;
port->mesh->now = mg_micros();
while (port->mesh->rx_len == 0 &&
(len = radio_receive_frame(frame, sizeof(frame), ifp)) != 0)
port->mesh->strategy->rx(port->mesh, frame, len);
s_mesh->tick(port->mesh, mg_micros());
return port->mesh->joined;
}
static bool radio_init(struct mg_tcpip_if *ifp) {
(void) ifp;
struct radio_port *port = ifp->driver_data;
char *buf = malloc(LAN_QUEUE_SIZE);
if (buf == NULL) return false;
mg_queue_init(&port->rx, buf, LAN_QUEUE_SIZE);
port->mesh = calloc(1, sizeof(*port->mesh));
if (port->mesh == NULL ||
!s_mesh->init(port->mesh, ifp, port->lan, port->address)) {
free(port->mesh);
port->mesh = NULL;
free(buf);
port->rx.buf = NULL;
return false;
}
port->mesh->strategy = s_mesh;
port->mesh->tx = radio_send;
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, radio_rx, radio_driver_poll};
static struct mg_tcpip_driver s_radio_driver = {radio_init, radio_tx, NULL,
radio_link};
#define MG_TCPIP_DRIVER_SIM_RADIO_INIT(manager, interface) \
do { \
struct mg_tcpip_if *mif_ = (interface); \
mif_->mgr = (manager); \
mg_tcpip_init((manager), mif_); \
MG_INFO(("Driver: sim_radio, MAC: %M", mg_print_mac, mif_->mac)); \
} while (0)
// A node's HTTP server: the test station requests GET /
#define MG_TCPIP_DRIVER_SIM_LAN_INIT(manager, interface) \
do { \
struct mg_tcpip_if *mif_ = (interface); \
mg_tcpip_init((manager), mif_); \
MG_INFO(("Driver: sim_lan, MAC: %M", mg_print_mac, mif_->mac)); \
} while (0)
// A node's HTTP server: the test node requests GET /
static void node_http_ev_handler(struct mg_connection *c, int ev,
void *ev_data) {
if (ev == MG_EV_HTTP_MSG) {
@@ -326,56 +589,38 @@ 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;
n->rif.driver_data = &n->radio;
n->radio.sim = sim;
n->radio.pos = &n->pos;
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);
MG_TCPIP_DRIVER_SIM_RADIO_INIT(&n->mgr, &n->rif);
if (n->mgr.ifp != NULL) {
mg_http_listen(&n->mgr, "http://0.0.0.0:80", node_http_ev_handler,
n->mgr.ifp->mac);
}
}
}
// Start each bridge: its LAN and radio drivers. No Mongoose, no IP
// Start each bridge: its LAN and radio drivers, with no IP stack.
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;
b->rif.driver_data = &b->radio;
b->radio.sim = sim;
b->radio.pos = &b->pos;
mg_mgr_init(&b->mgr);
b->radio.lan = &b->lif;
MG_TCPIP_DRIVER_SIM_RADIO_INIT(&b->mgr, &b->rif);
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);
b->lif.driver->init(&b->lif);
}
}
// 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[]) {
uint64_t poll_at = 0, mesh_at = (uint64_t) -1;
char *pending;
clock_t started; // CPU time, to show how fast the simulation runs
int i;
@@ -393,32 +638,82 @@ int main(int argc, char *argv[]) {
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 if (strcmp(argv[i], "-range") == 0 ||
strcmp(argv[i], "-loss") == 0) {
bool loss = strcmp(argv[i], "-loss") == 0;
const char *arg = argv[++i];
char *end;
double value = strtod(arg, &end);
if (end == arg || *end != '\0' ||
!(value >= 0 && value <= (loss ? 100 : 1e100)))
usage(&s_sim, argv[0]);
if (loss)
s_sim.radio_loss = value;
else
s_sim.radio_range = value;
} else if (strcmp(argv[i], "-m") == 0) {
const char *name = argv[++i];
if (strcmp(name, mg_mesh_a.name) == 0)
s_mesh = &mg_mesh_a;
else if (strcmp(name, mg_mesh_default.name) == 0)
s_mesh = &mg_mesh_default;
else
usage(&s_sim, argv[0]);
} 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);
s_sim.area.x, s_sim.area.y, s_sim.area.z, s_mesh->radio->name,
s_mesh->name, s_sim.num_bridges, s_sim.num_nodes);
check_area(&s_sim);
if (s_mesh->single_bridge && s_sim.num_bridges != 1) {
fprintf(stderr, "%s requires exactly one bridge\n", s_mesh->name);
return EXIT_FAILURE;
}
// mg_log_set(MG_LL_INFO);
mg_mgr_init(&s_sim.tester.mgr);
mg_tcpip_init(&s_sim.tester.mgr, &s_sim.tester.lif);
MG_TCPIP_DRIVER_SIM_LAN_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
// Drain packet processing at the current time. Only idle timer polling
// waits for STEP_MICROSECONDS; radio deadlines remain exact.
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;
radio_poll(&s_sim);
if (s_sim.now == poll_at || s_sim.now == mesh_at || s_sim.work_pending) {
do {
s_sim.work_pending = false;
dhcp_poll(&s_sim);
tester_poll(&s_sim);
{
struct bridge *b;
struct radio_node *n;
mesh_at = (uint64_t) -1;
for (b = s_sim.bridges; b != NULL; b = b->next) {
mg_mgr_poll(&b->mgr, 0);
if (b->radio.mesh->next_tick < mesh_at)
mesh_at = b->radio.mesh->next_tick;
}
for (n = s_sim.nodes; n != NULL; n = n->next) {
mg_mgr_poll(&n->mgr, 0);
if (n->radio.mesh->next_tick < mesh_at)
mesh_at = n->radio.mesh->next_tick;
}
}
if (mg_queue_next(&s_sim.tester.lif.recv_queue, &pending) > 0) {
s_sim.work_pending = true;
}
} while (s_sim.work_pending);
if (s_sim.now == poll_at) poll_at += STEP_MICROSECONDS;
}
s_sim.now = radio_next(&s_sim, mesh_at < poll_at ? mesh_at : poll_at);
if (s_sim.now > s_sim.duration) s_sim.now = s_sim.duration;
}
printf("Requests: %d ok, %d failed, of %d nodes", s_sim.tester.ok,
s_sim.tester.failed, s_sim.num_nodes);
@@ -426,8 +721,56 @@ int main(int argc, char *argv[]) {
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,
printf("\nSimulated %g s in %.3f s", (double) (int64_t) s_sim.now / 1e6,
(double) (clock() - started) / CLOCKS_PER_SEC);
if (s_sim.nodes != NULL) {
const struct radio_node *n;
size_t min = (size_t) -1, max = 0;
double total = 0;
for (n = s_sim.nodes; n != NULL; n = n->next) {
size_t peak = n->radio.mesh->ram_peak;
if (peak < min) min = peak;
if (peak > max) max = peak;
total += (double) peak;
}
printf("; peak mesh RAM per node (bytes): min %zu, max %zu, avg %.1f", min,
max, total / s_sim.num_nodes);
}
putchar('\n');
while (s_sim.air_head != NULL) {
struct radio_frame *f = s_sim.air_head;
s_sim.air_head = f->next;
free(f);
}
printf("Radio: %lu sent, %lu collided, %lu lost\n",
s_sim.air_sent, s_sim.air_collisions, s_sim.air_lost);
{
struct radio_node *n;
int count = 0, joined = 0;
for (n = s_sim.nodes; n != NULL; n = n->next) {
if (n->mgr.ifp != NULL && n->mgr.ifp->ip != 0) count++;
if (n->radio.mesh->joined) joined++;
}
printf("Nodes with IP: %d of %d, joined: %d of %d\n", count,
s_sim.num_nodes, joined, s_sim.num_nodes);
}
{
struct bridge *b;
struct radio_node *n;
mg_log_set(MG_LL_NONE);
for (b = s_sim.bridges; b != NULL; b = b->next) {
mg_mgr_free(&b->mgr);
s_mesh->free(b->radio.mesh);
free(b->radio.mesh);
free(b->radio.rx.buf);
}
for (n = s_sim.nodes; n != NULL; n = n->next) {
mg_mgr_free(&n->mgr);
s_mesh->free(n->radio.mesh);
free(n->radio.mesh);
free(n->radio.rx.buf);
}
}
return 0;
}
+50 -8
View File
@@ -5,22 +5,39 @@
#define MG_ENABLE_TCPIP 1
#include "l2_802154.h"
#include "l2_mesh.h"
#include "mongoose.h"
#include "star.h"
extern const struct mg_mesh mg_mesh_a;
// A position in metres
struct pos {
double x, y, z; // Coordinates
};
struct simulator;
// Private state of the simulated radio driver.
struct radio_port {
struct simulator *sim;
struct mg_tcpip_if *lan; // Bridge LAN interface, NULL for a node
const struct pos *pos; // Location of the radio
uint64_t tx_until; // Latest transmission end, us
uint64_t tx_ready_at; // Inter-frame wait ends, us
uint64_t rx_off_until; // Latest backoff/sensing/turnaround/TX end, us
struct mg_mesh_node *mesh; // Mesh state owned by the driver
struct mg_queue rx;
uint32_t random; // Driver backoff PRNG
uint16_t address; // Preassigned radio address
};
// 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_mgr mgr; // Polls the raw radio driver
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
struct radio_port radio;
};
// A radio node: Mongoose on a radio interface
@@ -29,31 +46,56 @@ struct radio_node {
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 radio_port radio;
};
struct simulator;
// LAN station: a DHCP server, 10.0.0.1. Gives each client an address made
// LAN node: 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
// LAN node: the test node, 10.0.0.2, a Mongoose instance. Waits for
// the mesh to settle, then cycles GET / through nodes until 5 s before the end
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 radio_node *next; // Node to request next, wraps at the end
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
};
enum { RADIO_BACKOFF, RADIO_CCA, RADIO_START, RADIO_END };
// One radio frame waiting on the simulated radio channel
struct radio_frame {
struct radio_frame *next;
struct mg_tcpip_if *from;
uint64_t due; // Next radio event, us
uint64_t start; // Transmission start, for receive overlap checks
uint8_t state, backoff;
bool collided;
size_t len;
uint8_t buf[1540];
};
uint64_t radio_airtime(size_t len);
// Simulator state
struct simulator {
struct radio_frame *air_head; // Pending and active frames, singly linked
uint64_t air_busy_until; // Last transmission end, for channel sensing
unsigned long air_sent, air_collisions;
unsigned long air_lost; // Receptions lost to range, random loss or TX overlap
double radio_range; // Maximum reception distance, metres
double radio_loss; // In-range reception loss probability, percent
uint32_t loss_random; // Reception-loss PRNG state
bool work_pending; // Drain immediate work before advancing time
struct pos area; // A box: all positions inside
struct bridge *bridges; // Bridges, a list
struct radio_node *nodes; // Radio nodes, a list
@@ -63,5 +105,5 @@ struct simulator {
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
struct lan_tester tester; // Test node on the LAN
};
-6
View File
@@ -1,6 +0,0 @@
// Copyright (c) 2026 Cesanta Software Limited
// All rights reserved
#include "star.h"
const struct mg_mesh mg_mesh_star = {"star"};
-10
View File
@@ -1,10 +0,0 @@
// 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;