mirror of
https://github.com/cesanta/mongoose.git
synced 2026-11-06 15:51:25 +00:00
Add radio simulation
This commit is contained in:
1 parent
9337a78cd0
commit
d39519b92f
66 files changed
+5659
-1000
No files matched your search
+2
-2
@@ -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
@@ -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
@@ -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.
|
||||
@@ -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"};
|
||||
@@ -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;
|
||||
@@ -1,4 +0,0 @@
|
||||
// Copyright (c) 2026 Cesanta Software Limited
|
||||
// All rights reserved
|
||||
|
||||
#include "l2_mesh.h"
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
@@ -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
@@ -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
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
// Copyright (c) 2026 Cesanta Software Limited
|
||||
// All rights reserved
|
||||
|
||||
#include "star.h"
|
||||
|
||||
const struct mg_mesh mg_mesh_star = {"star"};
|
||||
@@ -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;
|
||||
Reference in new issue
Block a user