Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1acad6dc4 | ||
|
|
2a91d186eb | ||
|
|
4827498188 | ||
|
|
3a0c08b695 | ||
|
|
7bdff8ff70 | ||
|
|
35b0590408 | ||
|
|
cbddf07bc8 | ||
|
|
871194517d | ||
|
|
5a1d2b9ef4 | ||
|
|
9cd3a86938 | ||
|
|
748668b8e9 | ||
|
|
778d1ad90f | ||
|
|
1ae4a538f5 | ||
|
|
c756bbe2c1 | ||
|
|
4c3ba612bb | ||
|
|
75b7a7df4f | ||
|
|
59025e4820 | ||
|
|
8110887be2 | ||
|
|
5f734dabf9 | ||
|
|
039ad42758 | ||
|
|
593909c26b | ||
|
|
0a7b3c723e | ||
|
|
29a61dc75c | ||
|
|
eead467ce6 | ||
|
|
f3ae02c425 | ||
|
|
d9cb74e4dd | ||
|
|
7ff6641f97 | ||
|
|
cd5d608e8d | ||
|
|
f7548e7c25 | ||
|
|
b960121464 | ||
|
|
b074645586 | ||
|
|
811dd427dd | ||
|
|
7f5184281d | ||
|
|
1c06b702dc | ||
|
|
59b4993861 | ||
|
|
4446b0f1a2 | ||
|
|
64fd61706d | ||
|
|
da61a0db7d | ||
|
|
8e99ffbe7e | ||
|
|
8877608858 | ||
|
|
a23f923e64 | ||
|
|
0522039830 | ||
|
|
dfcb685963 | ||
|
|
9bc25b34fd | ||
|
|
33319aa4e2 | ||
|
|
d79e62fd2a | ||
|
|
f6a954b97e | ||
|
|
94bb21ecc7 | ||
|
|
1bcabb893b | ||
|
|
10a7f1042b | ||
|
|
fefd424901 | ||
|
|
b4234b7f11 | ||
|
|
f63716c322 | ||
|
|
5512185cfe | ||
|
|
a8a785bbb7 | ||
|
|
5e2ca8aed4 | ||
|
|
0f854862e7 | ||
|
|
4553d1e0b1 | ||
|
|
784e3748e2 | ||
|
|
b11d29ff31 | ||
|
|
b246bcd72e | ||
|
|
33e2bb70e6 | ||
|
|
220bb4d186 | ||
|
|
6e810741f3 | ||
|
|
cdc47a2aea | ||
|
|
603cce2988 | ||
|
|
fcef46f4b0 | ||
|
|
d559af8477 | ||
|
|
bb86cf4e81 | ||
|
|
6ea0d5ebba | ||
|
|
b2bda3b07a | ||
|
|
41f53177a1 | ||
|
|
2d761f6453 | ||
|
|
7cb071c780 | ||
|
|
b7a9555905 | ||
|
|
0240a00d09 | ||
|
|
c0fcf807c0 | ||
|
|
55f40ecdfd | ||
|
|
90744ee0b7 | ||
|
|
1eb860a3fc | ||
|
|
4ee9598107 | ||
|
|
7066abbb86 | ||
|
|
21cef8c2e5 | ||
|
|
989b8620ba | ||
|
|
173ac58ed7 | ||
|
|
83adfd417a | ||
|
|
a0951f23c3 | ||
|
|
e19f531059 | ||
|
|
24d64a0013 | ||
|
|
3a87fc82c0 | ||
|
|
478444eb02 | ||
|
|
ad23c42fcc | ||
|
|
195f42af82 | ||
|
|
089af764ec | ||
|
|
7be5426f34 | ||
|
|
9ec63b5eb2 | ||
|
|
22a9346fe0 | ||
|
|
c0e52e6e1c | ||
|
|
11df30a85f | ||
|
|
8d8ff21e7c | ||
|
|
9c72767c01 | ||
|
|
6c7ffa1054 | ||
|
|
c0425d7444 | ||
|
|
d7db0f5829 | ||
|
|
f037ce2165 | ||
|
|
bc17d004ab | ||
|
|
048e5187ba | ||
|
|
4234fe6f86 | ||
|
|
126861fd16 | ||
|
|
47a6c4c6a0 | ||
|
|
06a6c3ee20 | ||
|
|
87f1f9d349 | ||
|
|
bfadf0c36a | ||
|
|
24c4162a75 | ||
|
|
b148fac340 | ||
|
|
8dde4eeee1 | ||
|
|
4ccdd80090 | ||
|
|
4d4e14600c | ||
|
|
aec0805a27 | ||
|
|
0bd8dee346 | ||
|
|
7800dc3c8d | ||
|
|
554188e90e | ||
|
|
fb678b9337 | ||
|
|
2828dbe4ca | ||
|
|
a8c8fd7002 | ||
|
|
e7c02da24b | ||
|
|
7347091055 | ||
|
|
55f15076ca | ||
|
|
7421953e8f | ||
|
|
9306e66067 | ||
|
|
439b87b860 | ||
|
|
d47301defc | ||
|
|
04b819a7b5 | ||
|
|
8e653122c7 | ||
|
|
7adfc3f992 | ||
|
|
ba9cadc14d | ||
|
|
924411de59 | ||
|
|
5ea3d143da | ||
|
|
d9195944df | ||
|
|
83a98c81f6 | ||
|
|
837637b70c | ||
|
|
7b3f58875a | ||
|
|
56c897e826 | ||
|
|
b2d980fc25 | ||
|
|
2cc13a1132 | ||
|
|
2ed7bba5e7 | ||
|
|
031f332ec1 | ||
|
|
22d50fe437 | ||
|
|
cde5a08bc5 | ||
|
|
55bf8c25fc | ||
|
|
399dde0f4b | ||
|
|
48747ee43d | ||
|
|
66971a0a26 | ||
|
|
4c24218afb | ||
|
|
7c27f4e2df | ||
|
|
4b4914736f | ||
|
|
92c0133ef9 | ||
|
|
a6b1a69630 | ||
|
|
6171ad8c14 | ||
|
|
28e705de5c | ||
|
|
d8b11f0b14 | ||
|
|
fcb9ec0c2d | ||
|
|
a4b55bc6f2 | ||
|
|
a710841723 | ||
|
|
fb1de111d7 | ||
|
|
b53fe7a1e7 | ||
|
|
2b5daf2438 | ||
|
|
db9fdd6794 | ||
|
|
9361b85f47 | ||
|
|
3b4c66439d | ||
|
|
68383c8bd5 | ||
|
|
76dea77929 | ||
|
|
d7ba178bf1 | ||
|
|
5f836cdf3b | ||
|
|
945f4780ea | ||
|
|
a8a5315460 | ||
|
|
0e38a15d46 | ||
|
|
d8e4389da2 | ||
|
|
25febfdeee | ||
|
|
f5be09c123 | ||
|
|
23321c4588 | ||
|
|
e1f5043489 | ||
|
|
5d9a2564e4 | ||
|
|
63bce1f01a | ||
|
|
84ce1ea147 | ||
|
|
4090d9f2b3 | ||
|
|
eba74fa6e2 | ||
|
|
8627bce1a1 | ||
|
|
d50caf231b | ||
|
|
f396200d38 | ||
|
|
6c04c37294 | ||
|
|
de23e5199d | ||
|
|
6b15571e14 | ||
|
|
8fd0a7f283 | ||
|
|
e589de2d6e | ||
|
|
a277108c84 | ||
|
|
9e26cc3795 | ||
|
|
84bb909437 | ||
|
|
2beebea453 | ||
|
|
d0cd8ec366 | ||
|
|
d31d0f85fe | ||
|
|
34aa5e995b | ||
|
|
c8dac10348 | ||
|
|
aab4cd086f | ||
|
|
2c8dec2fbd | ||
|
|
6208c243f9 | ||
|
|
c48b5ab556 | ||
|
|
3a87e746a8 | ||
|
|
cd04206334 | ||
|
|
8e55a6e4c1 | ||
|
|
f14ef121ef | ||
|
|
92263859a7 | ||
|
|
2768080edf | ||
|
|
466cc4cecd | ||
|
|
fe90c49795 | ||
|
|
23f92c1cbd | ||
|
|
31418ca821 | ||
|
|
0ee5777c15 | ||
|
|
79e7ed30f1 | ||
|
|
d5af07e458 | ||
|
|
7d957f8c7b | ||
|
|
5cae9e0183 | ||
|
|
edf660ccb3 | ||
|
|
e7b66281fa | ||
|
|
026213aab7 | ||
|
|
c1bee82baf | ||
|
|
0cab43fb43 | ||
|
|
47e129f4bd | ||
|
|
1341cd4078 | ||
|
|
4059202a5c | ||
|
|
d24d8806e1 | ||
|
|
a67eb15ad3 | ||
|
|
9e182a595c | ||
|
|
4587dc2d64 | ||
|
|
00fccd87f9 | ||
|
|
01bd4cfb73 | ||
|
|
125c1b7f13 | ||
|
|
d6cf67b6bc | ||
|
|
96dd647882 | ||
|
|
e42ff3590c | ||
|
|
7527233130 | ||
|
|
197226365b | ||
|
|
15b474172a |
@@ -0,0 +1,49 @@
|
||||
# Claude Code slash commands for the mcp-server test suite
|
||||
|
||||
Three AI-assisted workflows wrapping `mcp-server/run-tests.sh` and the meshtastic MCP tools. Each one has a twin in `.github/prompts/` for Copilot users.
|
||||
|
||||
| Slash command | What it does | Copilot equivalent |
|
||||
| --------------------- | ------------------------------------------------------------------------- | ---------------------------------------- |
|
||||
| `/test [args]` | Runs the test suite (auto-detects hardware) and interprets failures | `.github/prompts/mcp-test.prompt.md` |
|
||||
| `/diagnose [role]` | Read-only device health report via the meshtastic MCP tools | `.github/prompts/mcp-diagnose.prompt.md` |
|
||||
| `/repro <test> [n=5]` | Re-runs one test N times, diffs firmware logs between passes and failures | `.github/prompts/mcp-repro.prompt.md` |
|
||||
|
||||
## Why two surfaces
|
||||
|
||||
The Claude Code commands and Copilot prompts cover the same three workflows but each speaks its host's idiom:
|
||||
|
||||
- **Claude Code** (`/test`) uses `$ARGUMENTS` for pass-through, has direct access to Bash + all MCP tools registered in the user's settings, and runs in the terminal context.
|
||||
- **Copilot** (`/mcp-test`) runs in VS Code's agent mode; it has terminal + MCP access too but typically asks the operator to confirm inputs interactively.
|
||||
|
||||
A contributor using either IDE gets equivalent assistance. Keep the two in sync when behavior changes — the diff of intent should be minimal.
|
||||
|
||||
## House rules
|
||||
|
||||
- **No destructive writes without explicit operator approval.** Skills that could reflash, factory-reset, or reboot a device must describe the action and stop — the operator authorizes.
|
||||
- **Interpret failures, don't just echo them.** The skill body should pull firmware log lines from `mcp-server/tests/report.html` (the `Meshtastic debug` section, attached by `tests/conftest.py::pytest_runtest_makereport`) and classify the failure.
|
||||
- **Keep MCP tool calls sequential per port.** SerialInterface holds an exclusive port lock; two parallel tool calls on the same port deadlock.
|
||||
- **Never speculate about root cause.** If the evidence doesn't support a classification, say "unknown" and list what you'd need to disambiguate.
|
||||
|
||||
## Adding a new command
|
||||
|
||||
1. Write the Claude Code version at `.claude/commands/<name>.md` with YAML frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
description: one-line purpose (used for auto-invocation by the model)
|
||||
argument-hint: [optional-hint]
|
||||
---
|
||||
```
|
||||
|
||||
2. Write the Copilot equivalent at `.github/prompts/mcp-<name>.prompt.md` with:
|
||||
|
||||
```yaml
|
||||
---
|
||||
mode: agent
|
||||
description: ...
|
||||
---
|
||||
```
|
||||
|
||||
3. Add the row to the table above. Cross-link in both bodies.
|
||||
|
||||
4. Smoke-test on Claude Code first (`/<name>` should appear in autocomplete), then in VS Code Copilot (`/mcp-<name>` in Chat).
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
description: Produce a device health report using the meshtastic MCP tools (device_info, list_nodes, get_config, short serial log capture)
|
||||
argument-hint: [role=all|nrf52|esp32s3|<port>]
|
||||
---
|
||||
|
||||
# `/diagnose` — device health report
|
||||
|
||||
Call the meshtastic MCP tool bundle and format a structured health report for one or all detected devices. Zero guesswork for the operator.
|
||||
|
||||
## What to do
|
||||
|
||||
1. **Enumerate hardware.** Call `mcp__meshtastic__list_devices(include_unknown=True)`. For each entry where `likely_meshtastic=True`, capture `port`, `vid`, `pid`, `description`.
|
||||
|
||||
2. **Filter by `$ARGUMENTS`**:
|
||||
- No args, `all` → every likely-meshtastic device.
|
||||
- `nrf52` → only devices with `vid == 0x239a`.
|
||||
- `esp32s3` → only devices with `vid == 0x303a` or `vid == 0x10c4`.
|
||||
- A `/dev/cu.*` path → only that one port.
|
||||
- Anything else → treat as a substring match against the `port` string.
|
||||
|
||||
3. **For each selected device, in sequence (NOT parallel — SerialInterface holds an exclusive port lock):**
|
||||
- `mcp__meshtastic__device_info(port=<p>)` — captures `my_node_num`, `long_name`, `short_name`, `firmware_version`, `hw_model`, `region`, `num_nodes`, `primary_channel`.
|
||||
- `mcp__meshtastic__list_nodes(port=<p>)` — count of peers, which ones have `publicKey` set, SNR/RSSI distribution.
|
||||
- `mcp__meshtastic__get_config(section="lora", port=<p>)` — region, preset, channel_num, tx_power, hop_limit.
|
||||
- Optionally, if the device seems unhappy (fails to connect, `num_nodes==1` when ≥2 are plugged in, missing firmware*version), open a short firmware log window: `mcp__meshtastic__serial_open(port=<p>, env=<inferred-env>)`, wait 3s, `serial_read(session_id=<s>, max_lines=100)`, `serial_close(session_id=<s>)`. The env should be inferred from the VID map in `mcp-server/run-tests.sh` (nrf52 → rak4631, esp32s3 → heltec-v3) unless `MESHTASTIC_MCP_ENV*<ROLE>` is set.
|
||||
|
||||
4. **Hub health** (call once, not per-device): `mcp__meshtastic__uhubctl_list()` — enumerates every USB hub the host can see. Note which hubs advertise `ppps=true` and which hub hosts each Meshtastic device (cross-reference by VID). Flag it in the report if:
|
||||
- No hub advertises PPPS → `tests/recovery/` can't run on this setup; hard-recovery via `uhubctl_cycle` isn't available.
|
||||
- A Meshtastic device is on a non-PPPS hub → note it; operator may want to move the device to a PPPS hub to unlock auto-recovery.
|
||||
- `uhubctl_list` raises `ConfigError: uhubctl not found` → just say `uhubctl not installed` in the report; don't treat as a fault.
|
||||
|
||||
5. **Render per-device report** as:
|
||||
|
||||
```text
|
||||
[nrf52 @ /dev/cu.usbmodem1101] fw=2.7.23.bce2825, hw=RAK4631
|
||||
owner : Meshtastic 40eb / 40eb
|
||||
region/band : US, channel 88, LONG_FAST
|
||||
tx_power : 30 dBm, hop_limit=3
|
||||
peers : 1 (esp32s3 0x433c2428, pubkey ✓, SNR 6.0 / RSSI -24 dBm)
|
||||
primary ch : McpTest
|
||||
hub : 1-1.3 port 2 (PPPS, uhubctl-controllable)
|
||||
firmware : no panics in last 3s; NodeInfoModule emitted 2 broadcasts
|
||||
```
|
||||
|
||||
Keep it scannable. If a field is missing or abnormal (no pubkey for a known peer, region=UNSET, num_nodes inconsistent with the hub, device on non-PPPS hub), flag it inline with a short `⚠︎ <one-line reason>`.
|
||||
|
||||
6. **Cross-device correlation** (only when >1 device is inspected):
|
||||
- Do both sides see each other in `nodesByNum`? If one does and the other doesn't, that's asymmetric NodeInfo — flag it.
|
||||
- Do the LoRa configs match? (region, channel_num, modem_preset should all agree; mismatch = no mesh)
|
||||
- Do the primary channel NAMES match? Mismatch = different PSK = no decode.
|
||||
|
||||
7. **Recorder slice (cheap, always available).** The mcp-server runs an autouse log recorder that's been collecting from every connected device. Pull two short slices to surface anything weird that's already happened:
|
||||
- `mcp__meshtastic__logs_window(start="-2m", level="WARN|ERROR|CRIT", max_lines=20)` — recent firmware errors. If empty, say "no recent errors"; don't manufacture concern.
|
||||
- `mcp__meshtastic__telemetry_timeline(window="1h", field="free_heap", max_points=60)` — heap trend. If `slope_per_min < -50`, flag it and recommend `/leakhunt window=6h` for a deeper read; otherwise just note the current free heap.
|
||||
- If `recorder_status` shows `running:false` or `files.telemetry.last_ts` is null, note "recorder has no telemetry yet — enable `set_debug_log_api(True)` to populate" and skip this step gracefully.
|
||||
|
||||
8. **Suggest next actions only for specific, recognisable failure modes**:
|
||||
- Stale PKI pubkey one-way → "run `/test tests/mesh/test_direct_with_ack.py` — the retry + nodeinfo-ping heals this in the test path."
|
||||
- Region mismatch → "re-bake one side via `./mcp-server/run-tests.sh --force-bake`."
|
||||
- Device unreachable, reachable via DFU → `touch_1200bps(port=...)` + `pio_flash`. If not even DFU responds AND the device is on a PPPS hub, escalate to `uhubctl_cycle(role=..., confirm=True)`.
|
||||
- CP2102-wedged-driver on macOS → see the note in `run-tests.sh`.
|
||||
- Heap slope strongly negative → "run `/leakhunt window=6h` for a full timeline + classification."
|
||||
|
||||
## What NOT to do
|
||||
|
||||
- No writes. No `set_config`, no `reboot`, no `factory_reset`. This is a read-only diagnostic skill — if the operator wants to change state, they'll ask explicitly.
|
||||
- No `flash` / `erase_and_flash`. Those are separate escalations.
|
||||
- No holding SerialInterface across tool calls — open, query, close; next device. The port lock is exclusive.
|
||||
@@ -0,0 +1,103 @@
|
||||
---
|
||||
description: Hunt for memory leaks (and other slow degradations) by reading the persistent recorder's heap timeline + log slice over a window
|
||||
argument-hint: [window=1h] [field=free_heap] [variant=local]
|
||||
---
|
||||
|
||||
<!-- markdownlint-disable MD029 -->
|
||||
|
||||
# `/leakhunt` — read the recorder, classify a memory leak
|
||||
|
||||
Use the always-on recorder (`mcp-server/.mtlog/`) to read a heap timeline plus the matching log slice and produce a one-page verdict: **steady / slow leak / fragmentation / OOM-imminent**. No firmware changes, no special build flags — the LocalStats telemetry packet that the firmware already broadcasts every ~60 s carries `heap_free_bytes` and `heap_total_bytes`.
|
||||
|
||||
## Two signal paths — pick the right one
|
||||
|
||||
| Path | Build flag | Cadence | Per-thread attribution | Cost |
|
||||
| --------------------- | ---------------- | -------------- | ---------------------- | ------------------------- |
|
||||
| LocalStats packet | (default) | ~60 s | No | Free — always on |
|
||||
| `[heap N]` log prefix | `-DDEBUG_HEAP=1` | every log line | Yes (Thread X leaked) | Bigger flash + log volume |
|
||||
|
||||
Both feed the same `telemetry_timeline(field="free_heap")` query — when DEBUG_HEAP is on, the recorder synthesizes telemetry rows from log prefixes (tagged `source: debug_heap`), so a single timeline call gets whichever signal is available. **For a slow leak diagnosis, the default path is plenty** (60 s cadence over 6 h = 360 points; linear regression over that nails sub-100-byte/min slopes). **DEBUG_HEAP is for attribution** — when the slope is real and you need to know which thread is leaking.
|
||||
|
||||
## What to do
|
||||
|
||||
1. **Parse `$ARGUMENTS`**: optional `window` (default `1h`, accepts `30m`/`6h`/`-3d`/etc.), optional `field` (default `free_heap`; alternates: `total_heap`, `battery_level`, anything in the LocalStats variant), optional `variant` (default `local`; alternates: `device`, `environment`, `power`, `airQuality`, `health`).
|
||||
|
||||
2. **Verify the recorder is alive** — call `mcp__meshtastic__recorder_status`. Check:
|
||||
- `running == True`
|
||||
- `files.telemetry.lines > 0` (at least one telemetry packet recorded — if zero, the device hasn't broadcast LocalStats yet OR `set_debug_log_api` has never been on; tell the operator to run `mcp__meshtastic__set_debug_log_api(enabled=True)` and wait one device-update interval)
|
||||
- `files.telemetry.last_ts` within the last 5 minutes (if older, the device is silent — log that, not "leak detected")
|
||||
|
||||
3. **Detect whether DEBUG_HEAP is active** — `mcp__meshtastic__logs_window(start="-2m", grep=r"\\[heap \\d+\\]", max_lines=3)`. If any line matches, the firmware has the prefix → DEBUG_HEAP is on, expect higher-cadence data and `heap_event` rows. If zero matches over the last 2 minutes, you're on the LocalStats-only path.
|
||||
|
||||
4. **Pull the timeline** — `mcp__meshtastic__telemetry_timeline(window=$window, variant=$variant, field=$field, max_points=200)`. Read:
|
||||
- `samples` — how many raw points contributed
|
||||
- `min`, `max` — total swing
|
||||
- `slope_per_min` — units per minute (linear regression over the whole window)
|
||||
|
||||
5. **Pull the log context for the same window** — `mcp__meshtastic__logs_window(start="-${window}", grep="Heap status|leaked heap|freed heap|out of memory|Alloc an err|panic|abort", max_lines=200)`. These are the strings the firmware emits when something memory-related happens (`DEBUG_HEAP` builds emit `"Heap status:"` and `"leaked heap"` lines; production builds emit `"Alloc an err"` on failure and `"out of memory"` on OOM).
|
||||
|
||||
6. **Pull marker events** so we know if the operator labeled phases — `mcp__meshtastic__events_window(start="-${window}", kind="mark|connection_lost|connection_established")`. If a `connection_lost` overlaps a sharp drop, that's not a leak; that's a reboot.
|
||||
|
||||
6a. **(DEBUG_HEAP only) Per-thread attribution** — `mcp__meshtastic__logs_window(start="-${window}", grep="leaked heap", max_lines=200)`. Each row has a structured `heap_event` field with `{kind, thread, before, after, delta}`. Aggregate by thread: sum the `delta` over the window per thread name. The thread with the largest cumulative negative delta is your suspect. Note the count too — a thread with 50× small leaks is different from 1× big leak.
|
||||
|
||||
7. **Classify** based on what the data says, NOT on what you wish it said. Use these rules in order:
|
||||
- **Insufficient data** (< 5 samples): say so. Suggest a longer window or longer wait. Stop.
|
||||
- **Reboot mid-window**: if any `connection_lost` event is present AND `free_heap` jumped UP at that timestamp, the device rebooted. Note it; pre-reboot trend may be a leak but you only have part of the curve.
|
||||
- **OOM-imminent**: any `Alloc an err=` or `out of memory` line in the log slice. This trumps everything; flag urgently.
|
||||
- **Slow leak**: `slope_per_min < -50` AND `max - min > 1000` AND no reboot. The heap is monotonically (or near-monotonically) declining. Estimate time-to-zero: `min / -slope_per_min` minutes. Surface it.
|
||||
- **Fragmentation suspect**: `slope_per_min` close to zero (|x| < 50) BUT min trends down across the window AND the log slice shows `Alloc an err` warnings WITHOUT total OOM. Means free total is OK but largest contiguous block is shrinking. Recommend a `DEBUG_HEAP` build to confirm.
|
||||
- **Steady**: |slope_per_min| < 50, no error lines. Heap is fine.
|
||||
- **Recovery curve**: slope is POSITIVE — heap recovered. Either a workload completed or GC fired. Note it; not a leak.
|
||||
|
||||
8. **Report**:
|
||||
|
||||
```text
|
||||
/leakhunt window=6h field=free_heap variant=local
|
||||
────────────────────────────────────────────────────
|
||||
recorder : running, telem last_ts 8s ago
|
||||
build : DEBUG_HEAP=ON (per-line prefix detected)
|
||||
samples : 14,200 over 6h (cadence ~1.5s, log-line synth)
|
||||
free_heap : min 92,344 / max 124,008 / range 31,664
|
||||
slope : -82 bytes/min (negative — heap declining)
|
||||
reboots : none in window
|
||||
OOM events : none
|
||||
error lines : 3× "Alloc an err=ESP_ERR_NO_MEM" at +4h12m, +5h08m, +5h44m
|
||||
thread leaks : (DEBUG_HEAP) MeshPacket -3,124 B over 18 events
|
||||
Router -1,408 B over 4 events
|
||||
others -240 B
|
||||
verdict : SLOW LEAK — primary suspect MeshPacket thread
|
||||
est. time-to-OOM: ~1,127 min (~18.8 h) at current slope
|
||||
evidence : (3 log line citations with uptimes)
|
||||
```
|
||||
|
||||
Then: **what to do next.**
|
||||
- SLOW LEAK, **DEBUG_HEAP off** → recommend rebuilding with the flag and re-running this skill. Concrete one-liner the operator can copy:
|
||||
```text
|
||||
mcp__meshtastic__build(env="<env>", build_flags={"DEBUG_HEAP": 1})
|
||||
mcp__meshtastic__pio_flash(env="<env>", port="<port>", confirm=True)
|
||||
```
|
||||
After flash, set debug_log_api back on and wait one window; re-run `/leakhunt`.
|
||||
- SLOW LEAK, **DEBUG_HEAP on** → cite the top-leaking thread name from step 6a. Point at the corresponding source file (`grep -rn "ThreadName(\"<name>\")" src/`); the operator decides what to fix.
|
||||
- FRAGMENTATION SUSPECT → propose pre-allocating any per-packet buffers; or rebuilding with `CONFIG_HEAP_TASK_TRACKING=y` on ESP32 to see who's holding the largest blocks.
|
||||
- OOM-IMMINENT → flag for immediate attention; don't wait for the next telemetry interval.
|
||||
- STEADY → say so; stop. Don't invent problems.
|
||||
|
||||
## What NOT to do
|
||||
|
||||
- Don't assume a leak from a single dip. LocalStats fires every ~60 s and the firmware naturally allocates+frees on each broadcast cycle; one packet sees the trough. Look at the slope, not the deltas.
|
||||
- Don't recommend code changes. This skill diagnoses; the operator decides what to fix.
|
||||
- Don't enable `set_debug_log_api` automatically — if it's off, telemetry isn't reaching pubsub anyway, and the recorder will be empty. Tell the operator to flip it on and wait, then re-run.
|
||||
- Don't run heavy workloads to "trigger the leak." The recorder is passive; we read what's there.
|
||||
|
||||
## Companion: `mark_event` for stress runs
|
||||
|
||||
If the operator wants to test under stimulus (e.g. blast 50 broadcasts and see what the heap does), they can frame the experiment with markers:
|
||||
|
||||
```text
|
||||
mark_event("burst-start")
|
||||
… run the workload …
|
||||
mark_event("burst-end")
|
||||
/leakhunt window=15m
|
||||
```
|
||||
|
||||
The markers land in both `events.jsonl` and `logs.jsonl`, so the report can show "free_heap dipped 8 KB during the burst window, recovered to baseline within 2 LocalStats cycles" → not a leak.
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
description: Re-run a specific test N times in isolation to triage flakes, diff firmware logs between passes and failures
|
||||
argument-hint: <test-node-id> [count=5]
|
||||
---
|
||||
|
||||
<!-- markdownlint-disable MD029 -->
|
||||
|
||||
# `/repro` — flakiness triage for one test
|
||||
|
||||
Re-run a single pytest node ID N times in isolation, track pass rate, and surface what's _different_ in the firmware logs between the passing attempts and the failing ones. Turns "it's flaky, I guess" into "it fails when X, passes when Y."
|
||||
|
||||
## What to do
|
||||
|
||||
1. **Parse `$ARGUMENTS`**: first token is the pytest node id (e.g. `tests/mesh/test_direct_with_ack.py::test_direct_with_ack_roundtrip[nrf52->esp32s3]`); second token is an integer count (default `5`, cap at `20`). If the first token doesn't look like a test path (no `::` and no `tests/` prefix), treat the whole `$ARGUMENTS` as a `-k` filter instead.
|
||||
|
||||
2. **Sanity-check the hub first** (so we're not measuring "nothing plugged in" N times): call `mcp__meshtastic__list_devices`. If the test name contains `nrf52` or `esp32s3` and the matching VID isn't present, stop and report — re-running won't help.
|
||||
|
||||
3. **Loop N times**. For each iteration:
|
||||
|
||||
```bash
|
||||
./mcp-server/run-tests.sh <test-id> --tb=short -p no:cacheprovider
|
||||
```
|
||||
|
||||
Capture: exit code, duration, and (on failure) the `Meshtastic debug` firmware log section from `mcp-server/tests/report.html`. `-p no:cacheprovider` suppresses pytest's `.pytest_cache` writes so iterations don't influence each other.
|
||||
|
||||
4. **Track a small structured tally**:
|
||||
|
||||
```text
|
||||
attempt 1: PASS (42s)
|
||||
attempt 2: FAIL (128s) ← firmware log 200-line tail captured
|
||||
attempt 3: PASS (39s)
|
||||
attempt 4: FAIL (121s)
|
||||
attempt 5: PASS (41s)
|
||||
--------------------------------------
|
||||
pass rate: 3/5 (60%) | mean duration: 74s
|
||||
```
|
||||
|
||||
5. **On mixed outcomes**: diff the firmware log tails between a representative passing attempt and a representative failing attempt. Focus on:
|
||||
- Error-level lines only present in failures (`PKI_UNKNOWN_PUBKEY`, `Alloc an err=`, `Skip send`, `No suitable channel`)
|
||||
- Timing around the assertion event — did a broadcast go out, was there an ACK, did NAK fire?
|
||||
- Device state fields that changed (nodesByNum entries, region/preset, channel_num)
|
||||
|
||||
Surface the top 3 differences as a "passes when / fails when" table. Don't dump full logs — pull specific lines with uptime timestamps.
|
||||
|
||||
5a. **Archive recorder slices per attempt** (no extra device interaction; the recorder runs autouse). Right after each attempt finishes, capture its `(start_ts, end_ts)` and call `mcp__meshtastic__recorder_export(start=<start>, end=<end>, dest_dir="mcp-server/tests/repro_artifacts/<safe-test-id>/attempt_<n>/")`. This drops a `logs.jsonl`, `telemetry.jsonl`, `packets.jsonl`, and `events.jsonl` snapshot scoped to the attempt window. Use these for cross-attempt diffs in step 5: `jq '.line' logs.jsonl` is faster than re-running the test, and the telemetry slice lets you compare heap behavior across attempts.
|
||||
|
||||
6. **Classify the flake** into one of:
|
||||
- **LoRa airtime collision** → pass rate improves with fewer concurrent transmitters; propose a `time.sleep` gap or retry bump in the test body.
|
||||
- **PKI key staleness** → fails on first attempt, passes after self-heal; existing retry loop in `test_direct_with_ack.py` handles this.
|
||||
- **NodeInfo cooldown** → `Skip send NodeInfo since we sent it <600s ago` in fail-only logs; needs `broadcast_nodeinfo_ping()` warmup.
|
||||
- **Hardware-specific** (one direction fails, other passes; one device's firmware is older; driver wedged) → specific recovery pointer. For a device that's wedged past `touch_1200bps`, the next escalation is `uhubctl_cycle(role=..., confirm=True)` to hard-power-cycle its hub port (requires `uhubctl` installed).
|
||||
- **Device went dark mid-run** → fails from some attempt onward, never recovers, firmware log stops arriving. Almost always hardware: a Guru crash + frozen CDC. Hard-power-cycle via `uhubctl_cycle(role=..., confirm=True)` before the next iteration; if that also fails, escalate to replug.
|
||||
- **Genuinely unknown** → say so; don't invent a root cause.
|
||||
|
||||
7. **Report back** with:
|
||||
- Pass rate and mean duration.
|
||||
- Classification + evidence (the specific log lines that support it).
|
||||
- A suggested next step (re-run with specific args, open `/diagnose`, edit a specific test file, nothing).
|
||||
|
||||
## Examples
|
||||
|
||||
- `/repro tests/mesh/test_direct_with_ack.py::test_direct_with_ack_roundtrip[esp32s3->nrf52] 10` — runs 10 times, diffs firmware logs.
|
||||
- `/repro broadcast_delivers` — no `::`, no `tests/`, so interpreted as `-k broadcast_delivers`; runs every matching test the default 5 times.
|
||||
- `/repro tests/telemetry/test_device_telemetry_broadcast.py 3` — shorter run for a slow test.
|
||||
|
||||
## Constraints
|
||||
|
||||
- Don't exceed `count=20` per invocation — airtime and USB wear add up. If the user asks for 50, negotiate down.
|
||||
- Don't rebuild firmware as part of triage; flakes that only reproduce under different firmware belong in a separate session.
|
||||
- If the FIRST attempt fails AND the rest all pass, that's a classic "state leak from a prior test" → say so and suggest running with `--force-bake` or starting from a clean state rather than chasing the first failure.
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
description: Run the mcp-server test suite (auto-detects devices) and interpret the results
|
||||
argument-hint: [pytest-args]
|
||||
---
|
||||
|
||||
# `/test` — mcp-server test runner with interpretation
|
||||
|
||||
Run `mcp-server/run-tests.sh` and make sense of the output so the operator doesn't have to.
|
||||
|
||||
## What to do
|
||||
|
||||
1. **Invoke the wrapper.** From the firmware repo root, run:
|
||||
|
||||
```bash
|
||||
./mcp-server/run-tests.sh $ARGUMENTS
|
||||
```
|
||||
|
||||
The wrapper auto-detects connected Meshtastic devices, maps each to its PlatformIO env, exports the required `MESHTASTIC_MCP_ENV_*` env vars, and invokes pytest. If the user passed no arguments, the wrapper supplies a sensible default set (`tests/ --html=tests/report.html --self-contained-html --junitxml=tests/junit.xml -v --tb=short`). A `--report-log=tests/reportlog.jsonl` arg is always appended (unless the operator passed their own). `--assume-baked` is deliberately NOT in the defaults — `test_00_bake.py` has its own skip-if-already-baked check and runs the ~8 s verification by default. Operators can opt into the fast path with `--assume-baked`, or force a reflash with `--force-bake`.
|
||||
|
||||
2. **Read the pre-flight header.** First ~6 lines print the detected hub (role → port → env). If that line reads `detected hub : (none)`, the wrapper will narrow to `tests/unit` only — say so explicitly in your summary so the operator knows hardware tiers were skipped.
|
||||
|
||||
3. **On pass**: one-line summary of the form `N passed, M skipped in <duration>`. Don't enumerate the test names — the user can read those. Do mention any SKIPPED tests and name the cause:
|
||||
- `"role not present on hub"` → device unplugged; operator knows to reconnect.
|
||||
- `"firmware not baked with USERPREFS_UI_TEST_LOG"` → tests/ui skipped because the macro isn't in firmware yet; suggest `--force-bake`.
|
||||
- `"uhubctl not installed"` → tests/recovery + peer-offline skipped; suggest `brew install uhubctl` / `apt install uhubctl`.
|
||||
- `"no PPPS-capable hubs detected"` → tests/recovery skipped because the hub doesn't support per-port power; the tier will never run on that setup.
|
||||
- `"opencv-python-headless is not installed"` → tests/ui auto-deselected by run-tests.sh; suggest `pip install -e 'mcp-server/.[ui]'`.
|
||||
|
||||
4. **On failure**: for every FAILED test, open `mcp-server/tests/report.html` and extract the `Meshtastic debug` section for that test. pytest-html embeds the firmware log stream + device state dump there; the 200-line firmware log tail is usually enough to explain the failure. Summarise: which test, one-line assertion message, the firmware log lines that matter (things like `PKI_UNKNOWN_PUBKEY`, `Skip send NodeInfo`, `Error=`, `Guru Meditation`, `assertion failed`). For UI-tier failures also glance at `mcp-server/tests/ui_captures/<session>/<test>/transcript.md` — it records each step's frame + OCR.
|
||||
|
||||
5. **Classify the failure** as one of:
|
||||
- **Transient/flake**: LoRa collision, timing-sensitive assertion, first-attempt NAK + successful retry pattern. Propose `/repro <test_node_id>` to confirm.
|
||||
- **Environmental**: device unreachable, port busy, CP2102 driver wedged. Suggest the specific recovery in escalation order: (a) replug USB, (b) `touch_1200bps(port=...)` + `pio_flash` for nRF52 DFU, (c) `uhubctl_cycle(role="nrf52", confirm=True)` when a device is fully wedged past DFU (needs `uhubctl` installed — `baked_single`'s auto-recovery hook does this once automatically). Also check `git status userPrefs.jsonc`.
|
||||
- **Regression**: same assertion fails repeatedly, firmware log shows a new/unusual error. Surface the diff between expected and observed, identify the module likely responsible.
|
||||
|
||||
6. **Never run destructive recovery automatically.** If a failure looks like it needs a reflash, factory*reset, `uhubctl_cycle`, or USB replug, \_describe what to do* — don't execute. The operator decides.
|
||||
|
||||
## Arguments handling
|
||||
|
||||
- No args → wrapper's defaults (full suite).
|
||||
- `$ARGUMENTS` passed verbatim to the wrapper, which passes them to pytest.
|
||||
- Common operator invocations: `/test tests/mesh`, `/test tests/mesh/test_direct_with_ack.py::test_direct_with_ack_roundtrip`, `/test --force-bake`, `/test -k telemetry`.
|
||||
|
||||
## Side-effects to mention in summary
|
||||
|
||||
- The session fixture snapshots `userPrefs.jsonc` at session start and restores at teardown (plus on `atexit`). After a clean run, `git status userPrefs.jsonc` should be empty. If the wrapper's pre-flight printed a warning about a stale sidecar, call that out — means a prior session crashed.
|
||||
- `mcp-server/tests/report.html` and `junit.xml` are regenerated on every run; the HTML is self-contained (shareable).
|
||||
@@ -8,17 +8,21 @@
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/python:1": {
|
||||
"installTools": true,
|
||||
"version": "3.14"
|
||||
"version": "3.13"
|
||||
}
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"ms-vscode.cpptools",
|
||||
"platformio.platformio-ide",
|
||||
"Jason2866.esp-decoder",
|
||||
"pioarduino.pioarduino-ide",
|
||||
"Trunk.io"
|
||||
],
|
||||
"unwantedRecommendations": ["ms-azuretools.vscode-docker"],
|
||||
"unwantedRecommendations": [
|
||||
"ms-azuretools.vscode-docker",
|
||||
"platformio.platformio-ide"
|
||||
],
|
||||
"settings": {
|
||||
"extensions.ignoreRecommendations": true
|
||||
}
|
||||
|
||||
@@ -75,11 +75,11 @@ body:
|
||||
- type: checkboxes
|
||||
id: mui
|
||||
attributes:
|
||||
label: Is this bug report about any UI component firmware like InkHUD or Meshtatic UI (MUI)?
|
||||
label: Is this bug report about any UI (https://meshtastic.org/docs/configuration/device-uis/) component firmware?
|
||||
options:
|
||||
- label: Meshtastic UI aka MUI colorTFT
|
||||
- label: InkHUD ePaper
|
||||
- label: OLED slide UI on any display
|
||||
- label: Meshtastic UI aka MUI
|
||||
- label: InkHUD
|
||||
- label: BaseUI
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
|
||||
+514
-27
@@ -4,15 +4,16 @@ This document provides context and guidelines for AI assistants working with the
|
||||
|
||||
## Project Overview
|
||||
|
||||
Meshtastic is an open-source LoRa mesh networking project for long-range, low-power communication without relying on internet or cellular infrastructure. The firmware enables text messaging, location sharing, and telemetry over a decentralized mesh network.
|
||||
Meshtastic is an open-source LoRa mesh networking project for long-range, low-power communication without relying on internet or cellular infrastructure. The firmware enables text messaging, location sharing, and telemetry over a decentralized mesh network. The project uses **C++17** as its language standard across all platforms.
|
||||
|
||||
### Supported Hardware Platforms
|
||||
|
||||
- **ESP32** (ESP32, ESP32-S3, ESP32-C3) - Most common platform
|
||||
- **ESP32** (ESP32, ESP32-S3, ESP32-C3, ESP32-C6) - Most common platform
|
||||
- **nRF52** (nRF52840, nRF52833) - Low power Nordic chips
|
||||
- **RP2040/RP2350** - Raspberry Pi Pico variants
|
||||
- **STM32WL** - STM32 with integrated LoRa
|
||||
- **Linux/Portduino** - Native Linux builds (Raspberry Pi, etc.)
|
||||
- **macOS native** - Headless `meshtasticd` on Apple Silicon / x86_64; see `variants/native/portduino/platformio.ini` for Homebrew prereqs + CH341 LoRa setup
|
||||
|
||||
### Supported Radio Chips
|
||||
|
||||
@@ -70,6 +71,163 @@ PKI (Public Key Infrastructure) messages have special handling:
|
||||
- Accepted on a special "PKI" channel
|
||||
- Allow encrypted DMs between nodes that discovered each other on downlink-enabled channels
|
||||
|
||||
## Encryption & Key Management
|
||||
|
||||
Meshtastic packets on the air are typically encrypted one of two ways: the **per-channel symmetric** layer (AES-CTR with a shared PSK) for broadcasts and channel traffic, and the **per-peer PKI** layer (X25519 ECDH → AES-256-CCM) for direct messages and remote admin. A channel with a 0-byte PSK (or Ham mode, which wipes PSKs) transmits cleartext — see the size table below. Both are implemented in `src/mesh/CryptoEngine.cpp`; the send/receive dispatch lives in `src/mesh/Router.cpp`; admin authorization lives in `src/modules/AdminModule.cpp`.
|
||||
|
||||
### High-level model
|
||||
|
||||
- **Channels** are symmetric rooms: anyone with the PSK can read any message on the channel. Channel 0 is the "primary" channel and ships with the short-form default PSK on factory devices, forming the public mesh most users join. (The LoRa modem preset `LONG_FAST` lives on `config.lora.modem_preset` and is an independent field — don't conflate "channel 0 default PSK" with the modem preset name.)
|
||||
- **DMs** addressed to a single node require PKI so that other holders of the channel PSK can't read them. Outside Ham mode, Meshtastic does not fall back to channel-symmetric encryption when the destination public key is unknown.
|
||||
- **Remote admin** is a DM carrying an `AdminMessage`. The receiver only acts on it if the sender's public key is on its allowlist (`config.security.admin_key[0..2]`).
|
||||
- **Ham mode** (`owner.is_licensed=true`, where `owner` is the local `meshtastic_User` record) disables PKI entirely and sends cleartext — FCC Part 97 prohibits encryption on amateur bands.
|
||||
- **No ratchet, no session.** Every packet is encrypted from scratch — a stateless design that matches the high-loss, store-and-forward nature of LoRa.
|
||||
|
||||
### Symmetric channel encryption (AES-CTR)
|
||||
|
||||
`CryptoEngine::encryptPacket` / `decrypt` / `encryptAESCtr` in `src/mesh/CryptoEngine.cpp`.
|
||||
|
||||
- **Cipher**: AES-CTR, AES-128 or AES-256 depending on key length. Same routine in both directions (CTR is a stream cipher, so encrypt == decrypt).
|
||||
- **Key**: `ChannelSettings.psk` bytes. Size semantics:
|
||||
- **0 bytes** → no encryption, cleartext on the air
|
||||
- **1 byte** → short-form index into the well-known `defaultpsk[]` in `src/mesh/Channels.h`. Index 0 = cleartext; 1 = defaultpsk unchanged; 2..255 = defaultpsk with its last byte incremented by (index − 1). This is what the CLI's `--ch-set psk default` produces.
|
||||
- **16 bytes** → raw AES-128 key
|
||||
- **32 bytes** → raw AES-256 key
|
||||
- **2..15 bytes** → zero-padded to 16 and used as AES-128 (with a warn log); **17..31 bytes** → zero-padded to 32 and used as AES-256 (with a warn log). Defensive fallback for malformed PSK input, not something to rely on.
|
||||
- **Nonce (128 bit)**: `packet_id` (u64 LE) ‖ `from_node` (u32 LE) ‖ `block_counter` (u32, starts at 0). Built in `CryptoEngine::initNonce`.
|
||||
- **No AEAD**: channel packets carry no MAC, so the channel-hash byte is not an integrity or authenticity check. `Channels::getHash` is a 1-byte XOR-derived hint over the channel name bytes and PSK bytes that helps receivers pick a candidate channel/PSK for decryption. Because it is only a small hint and collisions are easy to find, it should be described purely as a PSK-selection aid, not as a security filter an attacker cannot bypass.
|
||||
- **Channel 0 is special in one way only**: it's the channel the Router attempts PKI decryption on before falling through to AES-CTR. Non-zero channels always go straight to AES-CTR.
|
||||
|
||||
### PKI encryption for DMs (X25519 ECDH + AES-256-CCM)
|
||||
|
||||
`CryptoEngine::encryptCurve25519` / `decryptCurve25519` in `src/mesh/CryptoEngine.cpp`.
|
||||
|
||||
- **Keypair**: Curve25519 (aka X25519), 32-byte public + 32-byte private. Stored in `config.security.public_key` / `private_key`; the public half is mirrored into `owner.public_key` so it rides along in NodeInfo broadcasts and propagates through the mesh like any other identity field.
|
||||
- **Key generation** (`generateKeyPair`): stirs `HardwareRNG::fill()` (64 B from platform TRNG when available), the 16-byte `myNodeInfo.device_id`, and a call to `random()` into the rweather/Crypto library's software RNG, then `Curve25519::dh1`. `regeneratePublicKey` recomputes the public half from a known private (used when restoring from backup).
|
||||
- **Keygen entry points**: at boot, `NodeDB` calls `generateKeyPair` (or `regeneratePublicKey` when a stored private key is present and passes a low-entropy check) **directly** when `!owner.is_licensed` and `config.lora.region != UNSET`. `ensurePkiKeys` wraps the same logic for runtime/admin flows — it's the path `AdminModule::handleSetConfig` runs when first assigning a valid region or when security config is written; **do not assume it's the universal boot-time gate**, because the NodeDB path bypasses it.
|
||||
- **Handshake**: `Curve25519::dh2(local_private, remote_public) → 32-byte shared secret → SHA-256 → 32-byte AES-256 key`. Recomputed per packet. The SHA-256 step is effectively a KDF over the raw ECDH output.
|
||||
- **Cipher**: AES-256-CCM via `aes_ccm_ae` / `aes_ccm_ad` (`src/mesh/aes-ccm.cpp`). MAC length (the `M` parameter) is **8 bytes**. No AAD — the MAC covers ciphertext only.
|
||||
- **Nonce (13 bytes / 104 bit)**: `aes_ccm_ae`/`aes_ccm_ad` use a 13-byte CCM nonce (`L = 2` is hardcoded in `src/mesh/aes-ccm.cpp`), not a 16-byte nonce. For PKI packets, `CryptoEngine::initNonce(fromNode, packetNum, extraNonce)` starts from the usual packet-derived nonce material, then overwrites nonce bytes `4..7` with a fresh 32-bit `extraNonce = random()`. The effective nonce bytes are therefore: bytes `0..3` = `packet_id`, bytes `4..7` = transmitted `extraNonce`, bytes `8..11` = `from_node`, byte `12` = `0x00`. The receiver reconstructs the same 13-byte nonce from the packet metadata plus the appended `extraNonce`.
|
||||
- **Wire overhead**: 12 bytes appended to the ciphertext = 8-byte MAC ‖ 4-byte extraNonce. Defined as `MESHTASTIC_PKC_OVERHEAD = 12` in `src/mesh/RadioInterface.h`. Only the 4-byte `extraNonce` is sent; the rest of the 13-byte CCM nonce is reconstructed from packet fields as described above. The Router's send path checks this overhead against `MAX_LORA_PAYLOAD_LEN` before committing to PKI.
|
||||
- **Send selection** (`Router::send`): the sender enters the PKI path when **all** hold — we're the originator AND not Ham mode AND not Portduino simradio AND not on the `serial`/`gpio` channels (unless the packet is already marked `pki_encrypted`) AND `config.security.private_key.size == 32` AND destination is a single node (not broadcast) AND the portnum isn't infrastructure. `TRACEROUTE_APP`, `NODEINFO_APP`, `ROUTING_APP`, and `POSITION_APP` are routed through channel encryption even when DMed (these need to be readable by relaying peers). Once on the PKI path, if the destination's public key isn't in our NodeDB the send **fails** with `PKI_SEND_FAIL_PUBLIC_KEY` — it does not silently fall back to channel encryption. If the client explicitly set `pki_encrypted=true` and any condition blocks PKI, the send fails with `PKI_FAILED`.
|
||||
- **Receive selection** (`Router::perhapsDecode`): try PKI decrypt first when `channel == 0` AND `isToUs(p)` AND not broadcast AND both peers have public keys in NodeDB AND `rawSize > MESHTASTIC_PKC_OVERHEAD`. On success the packet gets `pki_encrypted=true` stamped and the sender's public key copied into `p->public_key` for downstream authorization.
|
||||
|
||||
### Remote admin authorization
|
||||
|
||||
Implemented in `src/modules/AdminModule.cpp` → `handleReceivedProtobuf`. The authorization check runs in this order:
|
||||
|
||||
1. **Response messages** — if `messageIsResponse(r)` is true (the payload is a response to one of our earlier admin requests), it's accepted without any further check. The in-file comment flags this as a known-untightened gap: a stricter implementation would remember which `public_key` we last queried and reject responses that don't match.
|
||||
2. **Local admin** — `mp.from == 0` (phone app over BLE, serial CLI, internal module); never travels over the air. **Rejected** if `config.security.is_managed` is true, because managed devices expect admin to arrive over the air through an authorized remote path.
|
||||
3. **Legacy admin channel (deprecated)** — the packet arrived on a channel named literally `"admin"`. Gated by `config.security.admin_channel_enabled`; returns `NOT_AUTHORIZED` if the flag is false. Kept for backward compatibility; new deployments should use PKI admin.
|
||||
4. **PKI admin (preferred for remote)** — `mp.pki_encrypted == true` AND `mp.public_key` matches one of `config.security.admin_key[0..2]` (up to three authorized 32-byte Curve25519 public keys, typically copied from the admin node's own `user.public_key`).
|
||||
5. **Fallthrough** → `NOT_AUTHORIZED`.
|
||||
|
||||
On top of authorization, any remote admin message that **mutates** state (not a request, not a response) also has to pass a session-key check (`checkPassKey`): the client must first pull a fresh 8-byte `session_passkey` via `get_admin_session_key_request`, then echo that passkey back in the mutating message. The device rotates the passkey after 150 s and rejects values older than 300 s — a narrow anti-replay window on top of the PKI layer.
|
||||
|
||||
`config.security.is_managed = true` disables **local** admin writes (`mp.from == 0` is rejected). It does not by itself force every admin action through PKI — the legacy `"admin"` channel still authorizes remote admin when `config.security.admin_channel_enabled == true`. The AdminModule refuses to persist `is_managed=true` unless at least one `admin_key` is populated — a deliberate guard against operators locking themselves out.
|
||||
|
||||
### Key-rotation hazards (actions that invalidate peers)
|
||||
|
||||
- **`factory_reset_device`** (the "full" variant, calls `NodeDB::factoryReset(eraseBleBonds=true)`) → **wipes** the X25519 private key; a fresh keypair is generated on the next region-set. Every existing peer holds the old public key, so DMs to this node silently fail PKI decrypt until every peer re-exchanges NodeInfo.
|
||||
- **`factory_reset_config`** (the "partial" variant, calls `NodeDB::factoryReset()` with `eraseBleBonds=false`) → **preserves** the X25519 private key in `installDefaultConfig(preserveKey=true)`; the public key is zeroed and gets rebuilt from the preserved private key on the next boot via the NodeDB path's `regeneratePublicKey` call. Identity is preserved and the mesh does not need to re-exchange keys.
|
||||
- **`region=UNSET → valid region`** → `ensurePkiKeys` runs inside the same `handleSetConfig` path; missing keys get generated at that moment.
|
||||
- **Ham mode transitions** — entering Ham mode (`user.is_licensed=true`) runs `Channels::ensureLicensedOperation`, which **wipes every channel PSK** (all traffic becomes cleartext) and disables the legacy admin channel. The X25519 private key is preserved on the device but not used because `Router::send` skips PKI when `owner.is_licensed` is true. Leaving Ham mode re-enables PKI with the preserved keypair but does not restore the wiped channel PSKs — the operator has to re-set them.
|
||||
- **Channel 0 PSK change** → every peer must re-learn the channel hash; cached NodeInfo becomes temporarily unreachable until the next broadcast.
|
||||
- **`security.private_key` blanked via admin** → regenerates both halves (unless in Ham mode) and propagates the new public key via NodeInfo.
|
||||
|
||||
## NodeDB Layout (v25)
|
||||
|
||||
`DEVICESTATE_CUR_VER = 25`, `DEVICESTATE_MIN_VER = 24`. The on-device NodeDB was split in v25 into a slim header table plus four optional satellite stores. Older v24 saves auto-migrate at boot. Old training-data instincts (`node->user.long_name`, `node->position.latitude_i`, `node->is_favorite`, `node->device_metrics.battery_level`) are wrong now — the fields aren't there. Read this section before touching anything that walks `nodeDB->meshNodes`.
|
||||
|
||||
### Slim `NodeInfoLite`
|
||||
|
||||
`UserLite` is flattened onto `NodeInfoLite` (no nested sub-message); `position` and `device_metrics` are removed entirely (tags reserved). MAC address is dropped. Long names are capped at 25 chars (`max_size:25` in `deviceonly.options`); `hw_model` and `role` are `int_size:8`. Encoded size dropped from ~166 B → ~105 B per node.
|
||||
|
||||
Booleans are bit-packed into `NodeInfoLite.bitfield`. **Do not read or write the bits directly** — use the inline helpers in `src/mesh/NodeDB.h`:
|
||||
|
||||
```cpp
|
||||
nodeInfoLiteHasUser(n) // bit 5 — user fields populated
|
||||
nodeInfoLiteIsFavorite(n) // bit 3
|
||||
nodeInfoLiteIsIgnored(n) // bit 4
|
||||
nodeInfoLiteIsMuted(n) // bit 1
|
||||
nodeInfoLiteIsLicensed(n) // bit 6 — Ham mode peer
|
||||
nodeInfoLiteIsKeyManuallyVerified(n) // bit 0
|
||||
nodeInfoLiteHasIsUnmessagable(n) // bit 8 — "is_unmessagable was sent"
|
||||
nodeInfoLiteIsUnmessagable(n) // bit 7
|
||||
// via_mqtt is bit 2 (mask exposed; predicate uses the mask directly)
|
||||
|
||||
nodeInfoLiteSetBit(n, NODEINFO_BITFIELD_IS_FAVORITE_MASK, true); // setter
|
||||
```
|
||||
|
||||
### Satellite stores
|
||||
|
||||
Four `std::unordered_map<NodeNum, …>` members on `NodeDB`, each gated by its own build flag:
|
||||
|
||||
| Map | Value type | Build flag |
|
||||
| ----------------- | ------------------------------- | ---------------------------------- |
|
||||
| `nodePositions` | `meshtastic_PositionLite` | `MESHTASTIC_EXCLUDE_POSITIONDB` |
|
||||
| `nodeTelemetry` | `meshtastic_DeviceMetrics` | `MESHTASTIC_EXCLUDE_TELEMETRYDB` |
|
||||
| `nodeEnvironment` | `meshtastic_EnvironmentMetrics` | `MESHTASTIC_EXCLUDE_ENVIRONMENTDB` |
|
||||
| `nodeStatus` | `meshtastic_StatusMessage` | `MESHTASTIC_EXCLUDE_STATUSDB` |
|
||||
|
||||
Defaults are ON (i.e., maps **excluded**) for STM32WL only — see `src/mesh/mesh-pb-constants.h`. On every other arch all four maps are present. When excluded, the map member is absent and the corresponding accessors return `false`.
|
||||
|
||||
All four maps are guarded by **`mutable concurrency::Lock satelliteMutex`** — concurrent access from receive threads, the phone API state machine, and the renderer is the rule, not the exception.
|
||||
|
||||
### Accessor convention
|
||||
|
||||
**Never hand out pointers into the maps.** Use the copy-out accessors on `NodeDB`:
|
||||
|
||||
```cpp
|
||||
bool copyNodePosition(NodeNum, meshtastic_PositionLite &out) const;
|
||||
bool copyNodeTelemetry(NodeNum, meshtastic_DeviceMetrics &out) const;
|
||||
bool copyNodeEnvironment(NodeNum, meshtastic_EnvironmentMetrics &out) const;
|
||||
bool copyNodeStatus(NodeNum, meshtastic_StatusMessage &out) const;
|
||||
```
|
||||
|
||||
Each takes the lock, copies the value if present, returns `false` if the entry is absent or the DB is excluded. Pass-by-out-param is deliberate — pointer-style accessors would invite UAF and lock-leak bugs across the renderer. The "has any X" convenience predicates (`hasValidPosition` etc.) are implemented in terms of these.
|
||||
|
||||
Writers go through `setNodeStatus`, `updatePosition`, `updateTelemetry` (which dispatches on `which_variant` for device vs environment metrics) — these own the lock and the eviction hooks.
|
||||
|
||||
### Eviction
|
||||
|
||||
Every code path that drops a node from the header table must also evict the satellites. The single chokepoint is `eraseNodeSatellites(NodeNum)`; it's already called from `getOrCreateMeshNode`'s oldest-boring eviction, `removeNodeByNum`, both branches of `resetNodes`, `cleanupMeshDB`, `addFromContact`'s ignored-branch, and `AdminModule`'s `set_ignored_node`. Add new eviction sites here, not by calling `.erase()` directly.
|
||||
|
||||
### Sync flow: thin NodeInfo + post-COMPLETE_ID replay (no opt-in)
|
||||
|
||||
There is no capability flag and no special "gradient" nonce. The **default** sync flow is:
|
||||
|
||||
1. Config / module-config / channel / metadata segments (same as before).
|
||||
2. `STATE_SEND_OWN_NODEINFO` — **our own** NodeInfo, still bundled with our position and device_metrics (because the replay snapshot excludes our own NodeNum). Emitted via `ConvertToNodeInfo(lite)`.
|
||||
3. `STATE_SEND_OTHER_NODEINFOS` — every other peer's NodeInfo, **always thin** (no `position`, no `device_metrics`). Emitted via `ConvertToNodeInfoThin(lite)`.
|
||||
4. `STATE_SEND_FILEMANIFEST` → `STATE_SEND_COMPLETE_ID` — the phone sees `config_complete_id` and treats sync as done.
|
||||
5. `STATE_SEND_PACKETS` — live mesh packets, with a trailing replay drain interleaved. The replay drain walks four cached satellite stores in order (positions → telemetry → environment → status) and emits each cached entry as an ordinary `MeshPacket` on the matching portnum (`POSITION_APP`, `TELEMETRY_APP` device + environment variants, `NODE_STATUS_APP`). These are indistinguishable on the wire from live mesh traffic, so clients need no special handling — any code that already updates UI on `POSITION_APP` etc. works.
|
||||
|
||||
`PhoneAPI::sendConfigComplete()` arms `replayPhase = REPLAY_PHASE_POSITIONS` for default/full sync and `SPECIAL_NONCE_ONLY_NODES`, while `SPECIAL_NONCE_ONLY_CONFIG` skips replay. The drain runs inside `STATE_SEND_PACKETS` via `popReplayPacket()`, lower priority than live traffic. When all four phases drain, `replayPhase` flips back to `REPLAY_PHASE_IDLE` and the snapshot vectors get `shrink_to_fit`ed.
|
||||
|
||||
STM32WL and any other build with all four `MESHTASTIC_EXCLUDE_*DB` flags set produces zero replay packets — `popReplayPacket` advances through each phase in microseconds without emitting anything.
|
||||
|
||||
Special nonces that still mean something:
|
||||
|
||||
- `SPECIAL_NONCE_ONLY_CONFIG` (69420) — skip node sync entirely, just config.
|
||||
- `SPECIAL_NONCE_ONLY_NODES` (69421) — skip config segments, jump straight to `STATE_SEND_OWN_NODEINFO`. Still gets the post-COMPLETE_ID replay drain.
|
||||
|
||||
There are no other reserved nonces; everything else is a fresh random `want_config_id` from the client.
|
||||
|
||||
### v24 → v25 migration
|
||||
|
||||
The legacy migration code lives in **`src/mesh/NodeDBLegacyMigration.cpp`**, not in `NodeDB.cpp`. It owns the `meshtastic_NodeDatabase_Legacy` callback and `NodeDB::migrateLegacyNodeDatabase()`. The legacy proto descriptor is `protobufs/meshtastic/deviceonly_legacy.proto` (only included by the migration TU). The boot path peeks the file's leading version tag, runs the migration if `version < 25`, then re-saves in v25 layout. The legacy descriptor is scheduled for removal once `DEVICESTATE_MIN_VER` is bumped.
|
||||
|
||||
### Read-site rules of thumb
|
||||
|
||||
- Never `node->position.X` / `node->device_metrics.X` — those fields no longer exist. Pull from the satellite map via `copyNodePosition` / `copyNodeTelemetry`.
|
||||
- Never `node->user.long_name` — `long_name`, `short_name`, `public_key`, `hw_model`, `role`, `macaddr` (gone), `is_licensed`, `is_unmessagable` are flat on `NodeInfoLite`.
|
||||
- Never `node->is_favorite` / `node->is_ignored` / `node->via_mqtt` / `node->is_key_manually_verified` — use the bitfield helpers.
|
||||
- Never assume `nodeDB->getMeshNode(num)->position.time` — call `copyNodePosition` and check the return.
|
||||
- Don't lock `satelliteMutex` yourself in renderer code; the copy-out accessors already do.
|
||||
|
||||
Unit tests for the conversion layer live in `test/test_type_conversions/test_main.cpp` (Unity) — bitfield round-trips, `long_name` truncation, thin-vs-full conversions. Add cases there when extending the schema.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
@@ -80,21 +238,46 @@ firmware/
|
||||
│ │ ├── NodeDB.* # Node database management
|
||||
│ │ ├── Router.* # Packet routing
|
||||
│ │ ├── Channels.* # Channel management
|
||||
│ │ ├── CryptoEngine.* # AES-CTR (channels) + X25519 ECDH→AES-256-CCM (PKI for DMs/admin)
|
||||
│ │ ├── *Interface.* # Radio interface implementations
|
||||
│ │ ├── api/ # WiFi/Ethernet server APIs (ServerAPI, PacketAPI)
|
||||
│ │ ├── http/ # HTTP server (WebServer, ContentHandler)
|
||||
│ │ ├── wifi/ # WiFi support (WiFiAPClient)
|
||||
│ │ ├── eth/ # Ethernet support (ethClient)
|
||||
│ │ ├── udp/ # UDP multicast
|
||||
│ │ ├── compression/ # Message compression (unishox2)
|
||||
│ │ └── generated/ # Protobuf generated code
|
||||
│ ├── modules/ # Feature modules (Position, Telemetry, etc.)
|
||||
│ │ └── Telemetry/ # Telemetry subsystem
|
||||
│ │ └── Sensor/ # 50+ I2C sensor drivers
|
||||
│ ├── gps/ # GPS handling
|
||||
│ ├── graphics/ # Display drivers and UI
|
||||
│ ├── platform/ # Platform-specific code
|
||||
│ ├── input/ # Input device handling
|
||||
│ └── concurrency/ # Threading utilities
|
||||
│ │ └── niche/ # Specialized UIs (InkHUD e-ink framework)
|
||||
│ ├── platform/ # Platform-specific code (esp32, nrf52, rp2xx0, stm32wl, portduino)
|
||||
│ ├── input/ # Input device handling (InputBroker, keyboards, buttons)
|
||||
│ ├── detect/ # I2C hardware auto-detection (80+ device types)
|
||||
│ ├── motion/ # Accelerometer drivers (BMA423, BMI270, MPU6050, etc.)
|
||||
│ ├── mqtt/ # MQTT bridge client
|
||||
│ ├── power/ # Power HAL
|
||||
│ ├── nimble/ # BLE via NimBLE
|
||||
│ ├── buzz/ # Audio/notification (buzzer, RTTTL)
|
||||
│ ├── serialization/ # JSON serialization, COBS encoding
|
||||
│ ├── watchdog/ # Hardware watchdog thread
|
||||
│ ├── concurrency/ # Threading utilities (OSThread, Lock)
|
||||
│ ├── PowerFSM.* # Power finite state machine
|
||||
│ └── Observer.h # Observer/Observable event pattern
|
||||
├── variants/ # Hardware variant definitions
|
||||
│ ├── esp32/ # ESP32 variants
|
||||
│ ├── esp32s3/ # ESP32-S3 variants
|
||||
│ ├── nrf52/ # nRF52 variants
|
||||
│ └── rp2xxx/ # RP2040/RP2350 variants
|
||||
│ ├── esp32c3/ # ESP32-C3 variants
|
||||
│ ├── esp32c6/ # ESP32-C6 variants
|
||||
│ ├── nrf52840/ # nRF52 variants
|
||||
│ ├── rp2040/ # RP2040/RP2350 variants
|
||||
│ ├── stm32/ # STM32WL variants
|
||||
│ └── native/ # Linux/Portduino variants
|
||||
├── protobufs/ # Protocol buffer definitions
|
||||
├── boards/ # Custom PlatformIO board definitions
|
||||
├── test/ # Unit tests (12 test suites)
|
||||
└── bin/ # Build and utility scripts
|
||||
```
|
||||
|
||||
@@ -105,6 +288,9 @@ firmware/
|
||||
- Follow existing code style - run `trunk fmt` before commits
|
||||
- Prefer `LOG_DEBUG`, `LOG_INFO`, `LOG_WARN`, `LOG_ERROR` for logging
|
||||
- Use `assert()` for invariants that should never fail
|
||||
- C++17 features are available (`std::optional`, structured bindings, `if constexpr`, etc.)
|
||||
- **Keep code comments minimal — one or two lines, max.** Comment only when the _why_ isn't obvious from the code; never restate what the next line does. No multi-paragraph block comments explaining straightforward changes. The diff and commit message carry the rationale; the code carries the behavior.
|
||||
- **Use `Throttle` for time-based rate limiting, not raw `millis()` math.** `src/mesh/Throttle.h` provides `Throttle::isWithinTimespanMs(lastMs, intervalMs)` (returns true while inside the cooldown) and `Throttle::execute(&lastMs, intervalMs, func)` (function-pointer form that updates the timestamp on fire). Use these for any "did N ms pass since X" check — raw `millis() > lastMs + N` is rollover-unsafe (breaks after ~49.7 days) and inconsistent with the rest of the codebase. The helpers compute `now - lastMs` with unsigned subtraction, which wraps correctly.
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
@@ -118,70 +304,151 @@ firmware/
|
||||
|
||||
#### Module System
|
||||
|
||||
Modules inherit from `MeshModule` or `ProtobufModule<T>` and implement:
|
||||
Modules use a three-tier class hierarchy:
|
||||
|
||||
- `handleReceivedProtobuf()` - Process incoming packets
|
||||
- `allocReply()` - Generate response packets
|
||||
- `runOnce()` - Periodic task execution (returns next run interval in ms)
|
||||
1. **`MeshModule`** - Base class. Implement `wantPacket()` and `handleReceived()`. Returns `ProcessMessage::STOP` or `ProcessMessage::CONTINUE`.
|
||||
2. **`SinglePortModule`** - Handles a single portnum. Simplified `wantPacket()` that checks `decoded.portnum`.
|
||||
3. **`ProtobufModule<T>`** - Template for protobuf-based modules. Handles encoding/decoding automatically.
|
||||
|
||||
Most modules also inherit from **`OSThread`** for periodic tasks (the "mixin" pattern):
|
||||
|
||||
```cpp
|
||||
class MyModule : public ProtobufModule<meshtastic_MyMessage>
|
||||
class MyModule : public ProtobufModule<meshtastic_MyMessage>, private concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
MyModule();
|
||||
|
||||
protected:
|
||||
virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_MyMessage *msg) override;
|
||||
virtual int32_t runOnce() override;
|
||||
virtual meshtastic_MeshPacket *allocReply() override; // Generate response packets
|
||||
virtual int32_t runOnce() override; // Periodic task (returns next interval in ms)
|
||||
virtual bool alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_MyMessage *msg); // Modify in-flight
|
||||
virtual bool wantUIFrame(); // Request a UI display frame
|
||||
};
|
||||
```
|
||||
|
||||
Modules are registered in `src/modules/Modules.cpp` guarded by `MESHTASTIC_EXCLUDE_*` flags.
|
||||
|
||||
#### Observer/Observable Pattern
|
||||
|
||||
Event-driven communication between subsystems uses `src/Observer.h`:
|
||||
|
||||
```cpp
|
||||
// Observable emits events
|
||||
Observable<const meshtastic::Status *> newStatus;
|
||||
newStatus.notifyObservers(&status);
|
||||
|
||||
// Observer receives events via callback
|
||||
CallbackObserver<MyClass, const meshtastic::Status *> statusObserver =
|
||||
CallbackObserver<MyClass, const meshtastic::Status *>(this, &MyClass::handleStatusUpdate);
|
||||
```
|
||||
|
||||
#### Configuration Access
|
||||
|
||||
- `config.*` - Device configuration (LoRa, position, power, etc.)
|
||||
- `moduleConfig.*` - Module-specific configuration
|
||||
- `channels.*` - Channel configuration and management
|
||||
- `owner` - Device owner info
|
||||
- `myNodeInfo` - Local node info
|
||||
|
||||
#### Default Values
|
||||
|
||||
Use the `Default` class helpers in `src/mesh/Default.h`:
|
||||
|
||||
- `Default::getConfiguredOrDefaultMs(configured, default)` - Returns ms, using default if configured is 0
|
||||
- `Default::getConfiguredOrDefault(configured, default)` - Generic configured/default getter
|
||||
- `Default::getConfiguredOrMinimumValue(configured, min)` - Enforces minimum values
|
||||
- `Default::getConfiguredOrDefaultMsScaled(configured, default, numNodes)` - Scales based on network size
|
||||
|
||||
#### Thread Safety
|
||||
|
||||
- Use `concurrency::Lock` for mutex protection
|
||||
- Use `concurrency::Lock` and `concurrency::LockGuard` for mutex protection
|
||||
- Radio SPI access uses `SPILock`
|
||||
- Prefer `OSThread` for background tasks
|
||||
|
||||
### Hardware Detection
|
||||
|
||||
`src/detect/ScanI2C` automatically enumerates 80+ I2C device types at boot including displays, sensors, RTCs, keyboards, PMUs, and touch controllers. This drives automatic initialization of the correct drivers.
|
||||
|
||||
### Graphics/UI System
|
||||
|
||||
Multiple display driver families in `src/graphics/`:
|
||||
|
||||
- **OLED**: SSD1306, SH1106, ST7567
|
||||
- **TFT**: TFTDisplay (LovyanGFX-based)
|
||||
- **E-Ink**: EInkDisplay2, EInkDynamicDisplay, EInkParallelDisplay
|
||||
|
||||
**InkHUD** (`src/graphics/niche/InkHUD/`) is an event-driven e-ink UI framework:
|
||||
|
||||
- Applet-based architecture — modular display tiles
|
||||
- Read-only, static display optimized for minimal refreshes and low power
|
||||
- Configured per-variant via `nicheGraphics.h`
|
||||
- Separate PlatformIO config: `src/graphics/niche/InkHUD/PlatformioConfig.ini`
|
||||
|
||||
### Input System
|
||||
|
||||
`src/input/InputBroker` is the centralized input event dispatcher. Supports multiple input sources: buttons, keyboards (BBQ10, Cardputer, TCA8418), touch screens, rotary encoders, and matrix keyboards.
|
||||
|
||||
### Power Management
|
||||
|
||||
`src/PowerFSM.*` implements a finite state machine with states: `stateON`, `statePOWER`, `stateSERIAL`, `stateDARK`. Key events: `EVENT_PRESS`, `EVENT_WAKE_TIMER`, `EVENT_LOW_BATTERY`, `EVENT_RECEIVED_MSG`, `EVENT_SHUTDOWN`. Conditionally excluded with `MESHTASTIC_EXCLUDE_POWER_FSM` (falls back to `FakeFsm`).
|
||||
|
||||
### Motion Sensors
|
||||
|
||||
`src/motion/AccelerometerThread` provides background motion monitoring with automatic screen wake and double-tap button press detection. Supports 10+ accelerometer/gyroscope chips (BMA423, BMI270, MPU6050, LIS3DH, LSM6DS3, STK8XXX, QMA6100P, ICM20948, BMX160).
|
||||
|
||||
### Telemetry Sensor Library
|
||||
|
||||
`src/modules/Telemetry/Sensor/` contains 50+ I2C sensor drivers organized by category:
|
||||
|
||||
- **Power monitoring**: INA219/226/260/3221, MAX17048
|
||||
- **Environmental**: BME280/680, SCD4X (CO₂), SEN5X (particulate)
|
||||
- **Humidity/Temperature**: SHT3X/4X, AHT10, MCP9808, MLX90614
|
||||
- **Light**: BH1750, TSL2561/2591, VEML7700, LTR390UV, OPT3001
|
||||
- **Air quality**: PMSA003I, SFA30
|
||||
- **Specialized**: CGRadSens (radiation), NAU7802 (weight scale)
|
||||
|
||||
### API/Networking
|
||||
|
||||
`src/mesh/api/` provides a template-based `ServerAPI` for client communication over WiFi (`WiFiServerAPI`) and Ethernet (`ethServerAPI`). Default port: **4403**. HTTP server in `src/mesh/http/`. JSON serialization in `src/serialization/MeshPacketSerializer`.
|
||||
|
||||
### Hardware Variants
|
||||
|
||||
Each hardware variant has:
|
||||
|
||||
- `variant.h` - Pin definitions and hardware capabilities
|
||||
- `platformio.ini` - Build configuration
|
||||
- Optional: `pins_arduino.h`, `rfswitch.h`
|
||||
- Optional: `pins_arduino.h`, `rfswitch.h`, `nicheGraphics.h` (for InkHUD variants)
|
||||
|
||||
Key defines in variant.h:
|
||||
|
||||
```cpp
|
||||
#define USE_SX1262 // Radio chip selection
|
||||
#define HAS_GPS 1 // Hardware capabilities
|
||||
#define HAS_SCREEN 1 // Display present
|
||||
#define LORA_CS 36 // Pin assignments
|
||||
#define SX126X_DIO1 14 // Radio-specific pins
|
||||
```
|
||||
|
||||
### Protobuf Messages
|
||||
|
||||
- Defined in `protobufs/meshtastic/*.proto`
|
||||
- Generated code in `src/mesh/generated/`
|
||||
- Defined in `protobufs/meshtastic/*.proto` (~32 proto files)
|
||||
- Generated code in `src/mesh/generated/meshtastic/`
|
||||
- Regenerate with `bin/regen-protos.sh`
|
||||
- Message types prefixed with `meshtastic_`
|
||||
- Nanopb `.options` files control field sizes and encoding
|
||||
|
||||
### Conditional Compilation
|
||||
|
||||
```cpp
|
||||
#if !MESHTASTIC_EXCLUDE_GPS // Feature exclusion
|
||||
#if !MESHTASTIC_EXCLUDE_WIFI // Network feature exclusion
|
||||
#if !MESHTASTIC_EXCLUDE_BLUETOOTH // BLE exclusion
|
||||
#if !MESHTASTIC_EXCLUDE_POWER_FSM // Power FSM exclusion
|
||||
#ifdef ARCH_ESP32 // Architecture-specific
|
||||
#ifdef ARCH_NRF52 // Nordic platform
|
||||
#ifdef ARCH_RP2040 // Raspberry Pi Pico
|
||||
#ifdef ARCH_PORTDUINO // Linux native
|
||||
#if defined(USE_SX1262) // Radio-specific
|
||||
#ifdef HAS_SCREEN // Hardware capability
|
||||
#if USERPREFS_EVENT_MODE // User preferences
|
||||
@@ -189,10 +456,27 @@ Key defines in variant.h:
|
||||
|
||||
## Build System
|
||||
|
||||
## Agent Tooling Baseline
|
||||
|
||||
Mirror counterpart: `AGENTS.md` under **Agent Tooling Baseline**.
|
||||
|
||||
To reduce avoidable agent mistakes, assume these tools are available (or install them before significant repo work):
|
||||
|
||||
- **Required CLI basics**: `bash`, `git`, `find`, `grep`, `sed`, `awk`, `xargs`
|
||||
- **Strongly recommended**: `rg` (ripgrep) for fast file/text search, `jq` for JSON processing
|
||||
- **Build/test tools**: `python3`, `pip`, virtualenv (`python3 -m venv`), `platformio` (`pio`)
|
||||
- **Containerized native testing**: `docker` (fallback for non-Linux hosts; macOS can also build natively via `pio run -e native-macos`)
|
||||
|
||||
Fallback expectations for agents:
|
||||
|
||||
- If `rg` is unavailable, use `find` + `grep` instead of failing.
|
||||
- For native tests on hosts without Linux deps, prefer `./bin/test-native-docker.sh`.
|
||||
- The simulator helper script is `./bin/test-simulator.sh`.
|
||||
|
||||
Uses **PlatformIO** with custom scripts:
|
||||
|
||||
- `bin/platformio-pre.py` - Pre-build script
|
||||
- `bin/platformio-custom.py` - Custom build logic
|
||||
- `bin/platformio-custom.py` - Custom build logic, manifest generation
|
||||
|
||||
Build commands:
|
||||
|
||||
@@ -200,23 +484,41 @@ Build commands:
|
||||
pio run -e tbeam # Build specific target
|
||||
pio run -e tbeam -t upload # Build and upload
|
||||
pio run -e native # Build native/Linux version
|
||||
pio run -e native-macos # Build headless macOS meshtasticd (Homebrew prereqs in variants/native/portduino/platformio.ini)
|
||||
```
|
||||
|
||||
### Build Manifest
|
||||
|
||||
`bin/platformio-custom.py` emits a build manifest with metadata:
|
||||
|
||||
- `hasMui`, `hasInkHud` - UI capability flags (overridable via `custom_meshtastic_has_mui`, `custom_meshtastic_has_ink_hud`)
|
||||
- Architecture normalization (e.g., `esp32s3` → `esp32-s3` for API compatibility)
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Adding a New Module
|
||||
|
||||
1. Create `src/modules/MyModule.cpp` and `.h`
|
||||
2. Inherit from appropriate base class
|
||||
3. Register in `src/modules/Modules.cpp`
|
||||
4. Add protobuf messages if needed in `protobufs/`
|
||||
2. Inherit from appropriate base class (`MeshModule`, `SinglePortModule`, or `ProtobufModule<T>`)
|
||||
3. Mix in `concurrency::OSThread` if periodic work is needed
|
||||
4. Register in `src/modules/Modules.cpp` guarded by `#if !MESHTASTIC_EXCLUDE_MYMODULE`
|
||||
5. Add protobuf messages if needed in `protobufs/meshtastic/`
|
||||
6. Add test suite in `test/test_mymodule/` if applicable
|
||||
|
||||
### Adding a New Hardware Variant
|
||||
|
||||
1. Create directory under `variants/<arch>/<name>/`
|
||||
2. Add `variant.h` with pin definitions
|
||||
3. Add `platformio.ini` with build config
|
||||
4. Reference common configs with `extends`
|
||||
2. Add `variant.h` with pin definitions and hardware capability defines
|
||||
3. Add `platformio.ini` with build config — use `extends` to reference common base (e.g., `esp32s3_base`)
|
||||
4. Set `custom_meshtastic_support_level = 1` (PR builds) or `2` (merge builds)
|
||||
5. For e-ink displays, add `nicheGraphics.h` for InkHUD configuration
|
||||
|
||||
### Adding a New Telemetry Sensor
|
||||
|
||||
1. Create driver in `src/modules/Telemetry/Sensor/` following existing sensor pattern
|
||||
2. Register I2C address in `src/detect/ScanI2C` for auto-detection
|
||||
3. Integrate with the appropriate telemetry module (Environment, Health, Power, AirQuality)
|
||||
4. Add proto fields in `protobufs/meshtastic/telemetry.proto` if new data types are needed
|
||||
|
||||
### Modifying Configuration Defaults
|
||||
|
||||
@@ -305,9 +607,194 @@ Most workflows can be triggered manually via `workflow_dispatch` for testing.
|
||||
|
||||
## Testing
|
||||
|
||||
- Unit tests in `test/` directory
|
||||
- Run with `pio test -e native`
|
||||
- Use `bin/test-simulator.sh` for simulation testing
|
||||
### Native unit tests (C++)
|
||||
|
||||
Unit tests in `test/` directory with 12 test suites:
|
||||
|
||||
- `test_crypto/` - Cryptography
|
||||
- `test_mqtt/` - MQTT integration
|
||||
- `test_radio/` - Radio interface
|
||||
- `test_mesh_module/` - Module framework
|
||||
- `test_meshpacket_serializer/` - Packet serialization
|
||||
- `test_transmit_history/` - Retransmission tracking
|
||||
- `test_atak/` - ATAK integration
|
||||
- `test_default/` - Default configuration
|
||||
- `test_http_content_handler/` - HTTP handling
|
||||
- `test_serial/` - Serial communication
|
||||
|
||||
Run with: `pio test -e native`
|
||||
|
||||
Simulation testing: `bin/test-simulator.sh`
|
||||
|
||||
Quick entry point for new test modules: `test/README.md` (native unit-test authoring guide, skeleton, pitfalls, and setup checklist).
|
||||
|
||||
### Hardware-in-the-loop tests (`mcp-server/tests/`)
|
||||
|
||||
Separate pytest suite that exercises real USB-connected Meshtastic devices. See the **MCP Server & Hardware Test Harness** section below for invocation, tier layout, and agent usage rules.
|
||||
|
||||
## MCP Server & Hardware Test Harness
|
||||
|
||||
The `mcp-server/` directory houses a firmware-aware [MCP](https://modelcontextprotocol.io/) server plus a pytest-based integration suite. AI agents that speak MCP get a well-defined tool surface for flashing, configuring, and inspecting physical Meshtastic devices — use it instead of hand-rolling `pio` or `meshtastic --port` calls where possible. `mcp-server/README.md` is the operator-facing setup doc; this section is the agent-facing usage contract.
|
||||
|
||||
The repo registers the server via `.mcp.json` at the repo root — Claude Code picks it up automatically once `mcp-server/.venv/` is built (`cd mcp-server && python3 -m venv .venv && .venv/bin/pip install -e '.[test]'`).
|
||||
|
||||
### When to use which surface
|
||||
|
||||
| Goal | Tool |
|
||||
| ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
||||
| Find a connected device | `mcp__meshtastic__list_devices` |
|
||||
| Read a live node's config/state | `mcp__meshtastic__device_info`, `list_nodes`, `get_config` |
|
||||
| Mutate a device (owner, region, channels, reboot) | `set_owner`, `set_config`, `set_channel_url`, `reboot`, `shutdown`, `factory_reset` — all require `confirm=True` |
|
||||
| Flash firmware to a variant | `pio_flash` (any arch) or `erase_and_flash` (ESP32 factory install) |
|
||||
| Stream serial logs while debugging | `serial_open` → `serial_read` loop → `serial_close` |
|
||||
| Administer `userPrefs.jsonc` build-time constants | `userprefs_get`, `userprefs_set`, `userprefs_reset`, `userprefs_manifest` |
|
||||
| Run the regression suite | `./mcp-server/run-tests.sh` (or `/test` slash command) |
|
||||
| Diagnose a specific device | `/diagnose [role]` slash command (read-only) |
|
||||
| Triage a flaky test | `/repro <node-id> [count]` slash command |
|
||||
|
||||
**One MCP call per port at a time.** `SerialInterface` holds an exclusive OS-level lock on the serial port for its lifetime. If a `serial_*` session is open on `/dev/cu.usbmodem101`, calling `device_info` on the same port will fail fast pointing at the active session. Sequence calls: open → read/mutate → close, then next device. Never parallelize tool calls on the same port.
|
||||
|
||||
### MCP tool surface (43 tools)
|
||||
|
||||
Grouped by purpose. Full argument shapes in `mcp-server/README.md`; a few high-value signatures are called out here.
|
||||
|
||||
- **Discovery & metadata**: `list_devices`, `list_boards`, `get_board`
|
||||
- **Build & flash**: `build`, `clean`, `pio_flash`, `erase_and_flash` (ESP32 only), `update_flash` (ESP32 OTA), `touch_1200bps`
|
||||
- **Serial sessions** (long-running, 10k-line ring buffer): `serial_open`, `serial_read`, `serial_list`, `serial_close`
|
||||
- **Device reads**: `device_info`, `list_nodes`
|
||||
- **Device writes**: `set_owner`, `get_config`, `set_config`, `get_channel_url`, `set_channel_url`, `send_text`, `send_input_event` (inject a button/key press via the firmware's InputBroker), `set_debug_log_api`; destructive/power-state writes require `confirm=True`: `reboot`, `shutdown`, `factory_reset`
|
||||
- **userPrefs admin** (build-time constants, not runtime config): `userprefs_get`, `userprefs_set`, `userprefs_reset`, `userprefs_manifest`, `userprefs_testing_profile`
|
||||
- **Vendor escape hatches**: `esptool_chip_info`, `esptool_erase_flash`, `esptool_raw`, `nrfutil_dfu`, `nrfutil_raw`, `picotool_info`, `picotool_load`, `picotool_raw`
|
||||
- **USB power control** (via `uhubctl`, per-port PPPS toggle): `uhubctl_list` (read-only), `uhubctl_power(action='on'|'off', confirm=True)`, `uhubctl_cycle(delay_s, confirm=True)`. Target by raw `(location, port)` or by `role` (`"nrf52"`, `"esp32s3"`); role lookup checks `MESHTASTIC_UHUBCTL_LOCATION_<ROLE>` + `_PORT_<ROLE>` env vars first, falls back to VID auto-detection.
|
||||
- **Observability** (UI tier + operator ad-hoc): `capture_screen(role, ocr=True)` — grabs a USB-webcam frame of the device OLED and optionally OCRs it. Requires `mcp-server[ui]` extras (`opencv-python-headless`, `easyocr`) and `MESHTASTIC_UI_CAMERA_DEVICE_<ROLE>` env var; falls through to a 1×1 black PNG `NullBackend` when unconfigured.
|
||||
|
||||
`confirm=True` is a tool-level gate on top of whatever permission prompt your MCP host shows. **Don't bypass it** by asking the host to auto-approve — it exists specifically because MCP hosts sometimes remember "always allow this tool" and that's dangerous for `factory_reset`, `erase_and_flash`, `uhubctl_power(action='off')`, and `uhubctl_cycle`.
|
||||
|
||||
**TCP / native-host nodes.** Setting `MESHTASTIC_MCP_TCP_HOST=<host[:port]>` makes `list_devices` surface a `meshtasticd` daemon (e.g. the `native-macos` build) as a synthetic `tcp://host:port` entry, and `connect()` routes through `meshtastic.tcp_interface.TCPInterface` instead of `SerialInterface`. Every read/write/admin tool that flows through `connect()` works against the daemon transparently. USB-only tools (`pio_flash`, `erase_and_flash`, `update_flash`, `touch_1200bps`, `serial_open`, `esptool_*`, `nrfutil_*`, `picotool_*`) raise a clear `ConnectionError` when handed a `tcp://` port; `pio_flash` against a `native*` env raises a `FlashError` (no upload step — use `build` and run the binary directly). The pytest harness still assumes USB-attached devices per role; TCP-aware fixtures are deferred. See `mcp-server/README.md` § "TCP / native-host nodes".
|
||||
|
||||
### Hardware test suite (`mcp-server/run-tests.sh`)
|
||||
|
||||
The wrapper auto-detects connected devices (VID → role map: `0x239A` → `nrf52`, `0x303A`/`0x10C4` → `esp32s3`), maps each role to a PlatformIO env (`nrf52` → `rak4631`, `esp32s3` → `heltec-v3`, overridable via `MESHTASTIC_MCP_ENV_<ROLE>`), then invokes pytest. Zero pre-flight config needed from the operator.
|
||||
|
||||
Suite tiers (collected + run in this order via `pytest_collection_modifyitems`):
|
||||
|
||||
1. `tests/unit/` — pure Python (boards parse, pio wrapper, userPrefs parse, testing profile, uhubctl parser). No hardware.
|
||||
2. `tests/test_00_bake.py` — flashes each detected device with current `userPrefs.jsonc` merged with the session's test profile. Has its own skip-if-already-baked check comparing region + primary channel to the session profile; skips cheaply on warm devices.
|
||||
3. `tests/mesh/` — multi-device mesh: bidirectional send, broadcast delivery, direct-with-ACK, mesh formation within 60s. Parametrized `[nrf52->esp32s3]` and `[esp32s3->nrf52]`. Includes `test_peer_offline_recovery` which uses uhubctl to physically power off one peer mid-conversation (requires uhubctl; skips without).
|
||||
4. `tests/telemetry/` — `DEVICE_METRICS_APP` broadcast timing.
|
||||
5. `tests/monitor/` — boot-log panic check.
|
||||
6. `tests/recovery/` — `uhubctl` power-cycle round-trip + NVS persistence across hard reset. Requires `uhubctl` installed and a PPPS-capable hub; entire tier auto-skips otherwise.
|
||||
7. `tests/ui/` — input-broker-driven screen navigation with camera + OCR evidence.
|
||||
8. `tests/fleet/` — PSK seed session isolation.
|
||||
9. `tests/admin/` — channel URL roundtrip, owner persistence across reboot.
|
||||
10. `tests/provisioning/` — region + modem + slot bake, admin key presence, `UNSET` region blocks TX, userPrefs survive factory reset.
|
||||
|
||||
Invocation patterns:
|
||||
|
||||
```bash
|
||||
./mcp-server/run-tests.sh # full suite (auto-bake-if-needed)
|
||||
./mcp-server/run-tests.sh --force-bake # reflash before testing
|
||||
./mcp-server/run-tests.sh --assume-baked # skip bake (caller vouches for device state)
|
||||
./mcp-server/run-tests.sh tests/mesh # one tier
|
||||
./mcp-server/run-tests.sh tests/mesh/test_direct_with_ack.py # one file
|
||||
./mcp-server/run-tests.sh -k telemetry # name filter
|
||||
```
|
||||
|
||||
**No hardware detected?** The wrapper auto-narrows to `tests/unit/` only and prints `detected hub : (none)` in the pre-flight header. Agents interpreting the output should call this out explicitly — a 52-test green run without hardware is qualitatively different from a 12-unit-test green run.
|
||||
|
||||
**Artifacts every run produces:**
|
||||
|
||||
- `mcp-server/tests/report.html` — self-contained pytest-html. Each test gets a `Meshtastic debug` section with the tail of firmware log + device state dump. **Open this first** on failures; it's the canonical evidence source.
|
||||
- `mcp-server/tests/junit.xml` — CI-parseable.
|
||||
- `mcp-server/tests/reportlog.jsonl` — pytest-reportlog stream (`$report_type` keyed JSONL). Consumed by the live TUI.
|
||||
- `mcp-server/tests/fwlog.jsonl` — firmware log mirror from the `meshtastic.log.line` pubsub topic. Populated by the `_firmware_log_stream` autouse session fixture.
|
||||
|
||||
### Live TUI (`meshtastic-mcp-test-tui`)
|
||||
|
||||
A Textual-based live view that wraps `run-tests.sh`. Tails reportlog for per-test state, streams firmware logs, polls device state at startup + post-run (gated out of the active run because `hub_devices` holds exclusive port locks). Key bindings:
|
||||
|
||||
| Key | Action |
|
||||
| --- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| `r` | re-run focused test (leaf → that node id; internal node → directory or `-k`) |
|
||||
| `f` | filter tree by substring |
|
||||
| `d` | failure detail modal (pulls `longrepr` + captured stdout from the reportlog) |
|
||||
| `g` | export reproducer bundle (tar.gz with README, test_report.json, time-filtered fwlog, devices.json, env.json) |
|
||||
| `l` | toggle firmware log pane |
|
||||
| `x` | tool coverage modal |
|
||||
| `c` | cross-run history sparkline |
|
||||
| `q` | quit (SIGINT → SIGTERM → SIGKILL escalation, 5-s windows each) |
|
||||
|
||||
Launch:
|
||||
|
||||
```bash
|
||||
cd mcp-server
|
||||
.venv/bin/meshtastic-mcp-test-tui # full suite
|
||||
.venv/bin/meshtastic-mcp-test-tui tests/mesh # args pass through to pytest
|
||||
```
|
||||
|
||||
The plain CLI stays primary; the TUI is for operators who want a live dashboard. Both consume the same `run-tests.sh`.
|
||||
|
||||
### Slash commands (Claude Code + Copilot)
|
||||
|
||||
Three AI-assisted workflows wrap the test harness. Claude Code operators get `/test`, `/diagnose`, `/repro`; Copilot operators get `/mcp-test`, `/mcp-diagnose`, `/mcp-repro`. Bodies:
|
||||
|
||||
- `.claude/commands/{test,diagnose,repro}.md`
|
||||
- `.github/prompts/mcp-{test,diagnose,repro}.prompt.md`
|
||||
|
||||
`.claude/commands/README.md` is the index.
|
||||
|
||||
House rules for agents running these prompts:
|
||||
|
||||
- **Interpret failures, don't just echo them.** Pull firmware log tails from `report.html` and classify each failure as transient / environmental / regression. Use the exact format in `.claude/commands/test.md`.
|
||||
- **No destructive writes without operator approval.** Any skill that could reflash, factory-reset, or reboot a device must describe the action and stop. The operator authorizes.
|
||||
- **Sequential MCP calls per port.** See above.
|
||||
- **"Unknown" is a valid classification.** If evidence doesn't support a root cause, say so and list what would disambiguate. Do not invent.
|
||||
|
||||
### Key fixtures (test authors + agents debugging)
|
||||
|
||||
`mcp-server/tests/conftest.py` provides:
|
||||
|
||||
- **`_session_userprefs`** (autouse session) — snapshots `userPrefs.jsonc` at session start, merges the session test profile via `userprefs.merge_active(test_profile)`, restores at teardown. Four layers of safety: pytest teardown + `atexit` + sidecar file (`userPrefs.jsonc.mcp-session-bak`) + startup self-heal in `run-tests.sh`. **Do not edit `userPrefs.jsonc` from inside a test.**
|
||||
- **`_firmware_log_stream`** (autouse session) — subscribes to `meshtastic.log.line` pubsub on every connected `SerialInterface` and mirrors lines to `tests/fwlog.jsonl`. Drives the TUI firmware-log pane.
|
||||
- **`_debug_log_buffer`** (autouse per-test) — captures last 200 firmware log lines + device state for attachment to the pytest-html `Meshtastic debug` section on failure.
|
||||
- **`hub_devices`** (session) — `dict[role, SerialInterface]` with session-long exclusive port locks. Reason the TUI's device poller is gated to startup + post-run only.
|
||||
- **`baked_mesh`** — parametrized mesh-pair fixture; depends on `test_00_bake`. `pytest_generate_tests` in `conftest.py` auto-generates `[nrf52->esp32s3]` and `[esp32s3->nrf52]` variants.
|
||||
- **`test_profile`** — session-scoped dict: region, primary channel, admin key, PSK seed. Derived from `MESHTASTIC_MCP_SEED` (defaults to `mcp-<user>-<host>`).
|
||||
|
||||
### Firmware integration points tied to the test harness
|
||||
|
||||
Two firmware changes exist specifically so the test harness works reliably. **Keep these in mind when touching related code.**
|
||||
|
||||
- **`src/mesh/StreamAPI.cpp` + `StreamAPI.h`** — `emitLogRecord` uses a dedicated `fromRadioScratchLog` + `txBufLog` pair and a `concurrency::Lock streamLock`. Before this fix, `debug_log_api_enabled=true` would tear `FromRadio` protobufs on the serial transport because `emitTxBuffer` and `emitLogRecord` shared a single scratch buffer. The conftest enables the log stream session-wide; without this fix the device would corrupt its own FromRadio replies mid-session.
|
||||
- **`src/mesh/PhoneAPI.cpp`** — `ToRadio` `Heartbeat(nonce=1)` triggers `nodeInfoModule->sendOurNodeInfo(NODENUM_BROADCAST, true, 0, true)` for serial clients, mirroring the pre-existing behavior for TCP/UDP clients in `PacketAPI.cpp`. The mesh tests rely on this to force a NodeInfo broadcast right after connect so the peer discovers them before the test's first assertion.
|
||||
|
||||
If you're modifying `StreamAPI`, `PhoneAPI`, `NodeInfoModule`, or `userPrefs` flow, run `./mcp-server/run-tests.sh` at minimum before asking for review.
|
||||
|
||||
### Recovery playbooks
|
||||
|
||||
| Symptom | First check | Fix |
|
||||
| --------------------------------------------------------------------------------- | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `userPrefs.jsonc` dirty after test run | `git status --porcelain userPrefs.jsonc` | If non-empty, re-run `./mcp-server/run-tests.sh` once — the pre-flight self-heal restores from sidecar. If still dirty, `git checkout userPrefs.jsonc`. |
|
||||
| Port busy / wedged CP2102 on macOS | `lsof /dev/cu.usbserial-0001` | Kill the holder. USB replug if the kernel still reports busy. Often a stale `pio device monitor` or zombie `meshtastic_mcp` process. |
|
||||
| nRF52 appears unresponsive | `list_devices` shows VID `0x239A` but `device_info` times out | `touch_1200bps(port=...)` drops it into the DFU bootloader → `pio_flash` re-installs. |
|
||||
| Device fully wedged (Guru Meditation, frozen CDC, no DFU) | `list_devices` shows the VID but every admin call times out | `uhubctl_cycle(role="nrf52", confirm=True)` hard-power-cycles the port via USB hub PPPS. `baked_single`'s auto-recovery hook does this once automatically if uhubctl is installed. Falls back to physical replug if no PPPS hub. |
|
||||
| Multiple MCP server processes | `ps aux \| grep meshtastic_mcp` shows >1 | Kill all but the one your MCP host spawned. Zombies hold ports and break tests. |
|
||||
| Mesh formation fails, one side sees peer but other doesn't | `/diagnose` (or `list_nodes` on both sides) | Asymmetric NodeInfo. `test_direct_with_ack` has a heal path; `/repro` it a few times. If persistent, both devices' clocks may be out of sync with their NodeInfo cooldown. |
|
||||
| "role not present on hub" in skip reasons | `list_devices` | Expected if a device is unplugged. Reconnect before re-running the tier. |
|
||||
| Entire `tests/recovery/` tier skipped | `command -v uhubctl` | Expected if `uhubctl` isn't on PATH. Install via `brew install uhubctl` (macOS) or `apt install uhubctl` (Debian/Ubuntu). Also skips if no hub advertises PPPS. |
|
||||
| Entire `tests/ui/` tier skipped ("firmware not baked with USERPREFS_UI_TEST_LOG") | reportlog.jsonl for the skip reason | Re-run with `--force-bake` so the UI-log macro gets compiled into the fresh firmware. First run after the Round-3 landing always re-bakes. |
|
||||
| `tests/ui/` runs but captures are all 1×1 black PNGs | `MESHTASTIC_UI_CAMERA_DEVICE_ESP32S3` | Env var not set → `NullBackend`. Point a USB webcam at the heltec-v3 OLED and set the device index; `.venv/bin/python -c "import cv2; [print(i, cv2.VideoCapture(i).read()[0]) for i in range(5)]"` discovers it. |
|
||||
| Tests fail only on first attempt then pass on rerun | — | State leak from a prior session. Run with `--force-bake` to reset to a known state. |
|
||||
|
||||
### Never do these without asking
|
||||
|
||||
- `factory_reset` — wipes node identity; regenerates PKI keypair. Mesh peers will reject old DMs until re-exchange. Legitimate only when the operator explicitly wants it.
|
||||
- `erase_and_flash` — full chip erase; destroys all on-device state.
|
||||
- `esptool_erase_flash` / `esptool_raw` write/erase — bypasses pio's safety chain.
|
||||
- `set_config` on `lora.region` — changes regulatory domain; requires physical-location context the operator has and the agent doesn't.
|
||||
- `reboot` / `shutdown` mid-test — breaks fixture invariants.
|
||||
- `push -f`, `rebase -i`, `reset --hard`, or any history-rewriting git operation.
|
||||
- Clicking computer-use tools on web links in Mail/Messages/PDFs — open URLs via the claude-in-chrome MCP so the extension's link-safety checks apply.
|
||||
|
||||
## Resources
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
mode: agent
|
||||
description: Device health report via the meshtastic MCP tools (Copilot equivalent of the Claude Code /diagnose slash command)
|
||||
---
|
||||
|
||||
# `/mcp-diagnose` — device health report
|
||||
|
||||
Equivalent of `.claude/commands/diagnose.md`. Use when the operator asks to "check the devices", "what's the mesh looking like", "is nrf52 alive", etc.
|
||||
|
||||
This prompt assumes the meshtastic MCP server is registered with your VS Code Copilot agent. If it isn't, fall back to running `./mcp-server/run-tests.sh tests/unit` plus a short `device_info` script via the terminal.
|
||||
|
||||
## What to do
|
||||
|
||||
1. **Enumerate hardware** via the `list_devices` MCP tool (with `include_unknown=True`). For each entry where `likely_meshtastic=True`, capture `port`, `vid`, `pid`, `description`.
|
||||
|
||||
2. **Apply the operator's filter** (if any):
|
||||
- No filter → every likely-meshtastic device.
|
||||
- `nrf52` → `vid == 0x239a`
|
||||
- `esp32s3` → `vid == 0x303a` or `vid == 0x10c4`
|
||||
- A `/dev/cu.*` path → only that port.
|
||||
- Anything else → substring match on port.
|
||||
|
||||
3. **For each selected device, in sequence (don't parallelize — SerialInterface holds an exclusive port lock):**
|
||||
- `device_info(port=<p>)` → `my_node_num`, `long_name`, `short_name`, `firmware_version`, `hw_model`, `region`, `num_nodes`, `primary_channel`
|
||||
- `list_nodes(port=<p>)` → peer count, which peers have `publicKey`, SNR/RSSI distribution
|
||||
- `get_config(section="lora", port=<p>)` → region, preset, channel_num, tx_power, hop_limit
|
||||
- If anything looks off (can't connect, `num_nodes` wrong, missing `firmware_version`), open a short firmware-log window: `serial_open(port=<p>, env=<inferred>)`, wait 3 seconds, `serial_read(session_id, max_lines=100)`, `serial_close(session_id)`. Infer env from VID (0x239a → `rak4631`, 0x303a/0x10c4 → `heltec-v3`) unless an `MESHTASTIC_MCP_ENV_<ROLE>` env var overrides it.
|
||||
|
||||
4. **Hub health** (call once, not per-device): `uhubctl_list()` — enumerates every USB hub the host sees. Cross-reference each Meshtastic device's VID to find which hub + port it's on. Flag in the report if:
|
||||
- No hub advertises `ppps=true` → `tests/recovery/` can't run; hard-recovery via `uhubctl_cycle` isn't available.
|
||||
- A Meshtastic device is on a non-PPPS hub → note it; moving to a PPPS hub unlocks auto-recovery.
|
||||
- `uhubctl_list` raises `ConfigError: uhubctl not found` → report as "uhubctl not installed"; don't treat as a device fault.
|
||||
|
||||
5. **Render per-device report** as a compact block:
|
||||
|
||||
```text
|
||||
[nrf52 @ /dev/cu.usbmodem1101] fw=2.7.23.bce2825, hw=RAK4631
|
||||
owner : Meshtastic 40eb / 40eb
|
||||
region/band : US, channel 88, LONG_FAST
|
||||
tx_power : 30 dBm, hop_limit=3
|
||||
peers : 1 (esp32s3 0x433c2428, pubkey ✓, SNR 6.0 / RSSI -24 dBm)
|
||||
primary ch : McpTest
|
||||
hub : 1-1.3 port 2 (PPPS, uhubctl-controllable)
|
||||
firmware : no panics in last 3s
|
||||
```
|
||||
|
||||
Flag abnormalities inline with `⚠︎ <short reason>` — missing pubkey on a known peer, region UNSET, mismatched channel name, device on non-PPPS hub, etc.
|
||||
|
||||
6. **Cross-device correlation** (when >1 device selected):
|
||||
- Do both see each other in `nodesByNum`?
|
||||
- Do `region`, `channel_num`, `modem_preset` match across devices?
|
||||
- Do the primary channel names match? (Different name → different PSK → no decode.)
|
||||
|
||||
7. **Suggest next steps only for recognizable failure modes**, never speculatively:
|
||||
- Stale PKI one-way → "`/mcp-test tests/mesh/test_direct_with_ack.py` — the test's retry+nodeinfo-ping heals this."
|
||||
- Region mismatch → "re-bake one side via `./mcp-server/run-tests.sh --force-bake`."
|
||||
- Device unreachable, DFU reachable → `touch_1200bps(port=...)` + `pio_flash`. If not even DFU responds and the device is on a PPPS hub, escalate to `uhubctl_cycle(role=..., confirm=True)`.
|
||||
- CP2102-wedged-driver on macOS → see `run-tests.sh` notes.
|
||||
|
||||
## Hard constraints
|
||||
|
||||
- **Read-only.** No `set_config`, no `reboot`, no `factory_reset`, no `flash`. If the operator wants mutation, they'll escalate explicitly.
|
||||
- **Open/query/close per device.** Never hold multiple SerialInterfaces to the same port. The port lock is exclusive.
|
||||
- **Don't infer env beyond the VID map** — if the operator has an unusual board, ask them which env to use rather than guessing.
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
mode: agent
|
||||
description: Re-run a specific test N times to triage flakes; diff firmware logs between passes and failures (Copilot equivalent of the Claude Code /repro slash command)
|
||||
---
|
||||
|
||||
# `/mcp-repro` — flakiness triage for one test
|
||||
|
||||
Equivalent of `.claude/commands/repro.md`. Use when the operator says "that one test is flaky — dig in", "repro the direct_with_ack failure", "why does X sometimes fail?".
|
||||
|
||||
## What to do
|
||||
|
||||
1. **Parse the operator's input** into two pieces:
|
||||
- **Test identifier** — either a pytest node id (has `::` or starts with `tests/`) or a `-k`-style filter (plain substring like `direct_with_ack`).
|
||||
- **Count** — integer, default `5`, cap at `20`. If the operator asks for 50, negotiate down and explain (airtime + USB wear).
|
||||
|
||||
2. **Sanity-check the hub** via the `list_devices` MCP tool. If the test name references `nrf52` or `esp32s3` and the matching VID isn't present, stop and report — re-running won't help.
|
||||
|
||||
3. **Loop** N times. Each iteration:
|
||||
|
||||
```bash
|
||||
./mcp-server/run-tests.sh <test-id> --tb=short -p no:cacheprovider
|
||||
```
|
||||
|
||||
`-p no:cacheprovider` keeps pytest from caching anything between iterations. Capture: exit code, duration, and (on failure) the `Meshtastic debug` firmware-log section from `mcp-server/tests/report.html`.
|
||||
|
||||
4. **Tally** results as you go:
|
||||
|
||||
```text
|
||||
attempt 1: PASS (42s)
|
||||
attempt 2: FAIL (128s) ← fw log captured
|
||||
attempt 3: PASS (39s)
|
||||
attempt 4: FAIL (121s)
|
||||
attempt 5: PASS (41s)
|
||||
--------------------------------------------------
|
||||
pass rate: 3/5 (60%) | mean duration: 74s
|
||||
```
|
||||
|
||||
5. **On mixed outcomes, diff the firmware logs** between one representative pass and one representative fail. Focus on:
|
||||
- Error-level lines present only in failures (`PKI_UNKNOWN_PUBKEY`, `Alloc an err=`, `Skip send`, `No suitable channel`, `NAK`)
|
||||
- Timing around the assertion point (broadcast sent? ACK received? retry fired?)
|
||||
- Device-state fields that changed between attempts
|
||||
|
||||
Surface the top 3 differences as a compact "passes when / fails when" table with uptime timestamps. Don't dump full logs.
|
||||
|
||||
6. **Classify** the flake into one of:
|
||||
- **LoRa airtime collision** — pass rate improves with fewer concurrent transmitters. Suggest a `time.sleep` gap or retry bump in the test body.
|
||||
- **PKI key staleness** — first attempt fails, subsequent ones pass; existing retry-loop pattern in `test_direct_with_ack.py` is the fix.
|
||||
- **NodeInfo cooldown** — `Skip send NodeInfo since we sent it <600s ago` in fail-only logs; needs a `broadcast_nodeinfo_ping()` warmup.
|
||||
- **Hardware-specific** — one direction consistently fails, firmware versions differ, CP2102 driver wedged, etc. For a device wedged past `touch_1200bps`, recommend `uhubctl_cycle(role=..., confirm=True)` to hard-power-cycle its hub port (requires `uhubctl` installed).
|
||||
- **Device went dark mid-run** — fails from some iteration onward and never recovers; firmware log stops arriving. Almost always a Guru crash with frozen CDC. Recommend `uhubctl_cycle` before the next iteration; escalate to replug if that also fails.
|
||||
- **Unknown** — say so. Don't invent a root cause.
|
||||
|
||||
7. **Report back** with:
|
||||
- Pass rate + mean duration.
|
||||
- Classification + the specific log evidence for it.
|
||||
- A concrete next step (tighter assertion, more retries, open `/mcp-diagnose`, file a bug, nothing).
|
||||
|
||||
## Examples
|
||||
|
||||
- `tests/mesh/test_direct_with_ack.py::test_direct_with_ack_roundtrip[esp32s3->nrf52] 10` — 10 runs of that parametrized case.
|
||||
- `broadcast_delivers` — no `::`, no `tests/`; treat as `-k broadcast_delivers`; runs every match 5 times.
|
||||
- `tests/telemetry/test_device_telemetry_broadcast.py 3` — shorter count for a slow test.
|
||||
|
||||
## Notes
|
||||
|
||||
- If the FIRST attempt fails and the rest pass, that's a state-leak signature — suggest starting from `--force-bake` or a clean device state rather than chasing the first-failure firmware logs.
|
||||
- If ALL N fail, this isn't a flake — it's a regression. Say so, stop iterating, escalate to `/mcp-test` for full-suite context.
|
||||
- Don't rebuild firmware during triage. Flakes that only reproduce under different firmware belong in a separate session with a plan.
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
mode: agent
|
||||
description: Run the mcp-server test suite and interpret results (Copilot equivalent of the Claude Code /test slash command)
|
||||
---
|
||||
|
||||
# `/mcp-test` — mcp-server test runner with interpretation
|
||||
|
||||
Equivalent of the Claude Code `/test` slash command in `.claude/commands/test.md`. Use this when the operator asks you to "run the tests", "check the mcp test suite", "run the mesh tests", etc.
|
||||
|
||||
## What to do
|
||||
|
||||
1. **Invoke the wrapper** from the firmware repo root:
|
||||
|
||||
```bash
|
||||
./mcp-server/run-tests.sh [pytest-args]
|
||||
```
|
||||
|
||||
If the operator specified a subset (e.g. "just the mesh tests"), pass it through as `tests/mesh` or a pytest `-k filter`. If they said nothing, use the wrapper's defaults (full suite with pytest-html report).
|
||||
|
||||
The wrapper auto-detects connected Meshtastic devices, maps each to its PlatformIO env, exports the required env vars, and invokes pytest. Zero pre-flight config needed from the operator.
|
||||
|
||||
2. **Read the pre-flight header** (first few lines of wrapper output). The `detected hub :` line lists role → port → env mappings. If it reads `(none)`, the wrapper narrowed to `tests/unit` only — call that out explicitly so the operator knows hardware tiers were skipped.
|
||||
|
||||
3. **On pass**: one-line summary like `N passed, M skipped in <duration>`. Don't enumerate test names. DO mention any non-placeholder SKIPs and name the cause:
|
||||
- `"role not present on hub"` → device unplugged; operator should reconnect.
|
||||
- `"firmware not baked with USERPREFS_UI_TEST_LOG"` → tests/ui skipped; the UI-log compile macro isn't in the baked firmware. Suggest `--force-bake`.
|
||||
- `"uhubctl not installed"` → tests/recovery + `test_peer_offline_recovery` skipped. Suggest `brew install uhubctl` / `apt install uhubctl`.
|
||||
- `"no PPPS-capable hubs detected"` → tests/recovery skipped because the attached hub doesn't support per-port power switching; won't run on that setup.
|
||||
- `"opencv-python-headless is not installed"` → tests/ui auto-deselected by `run-tests.sh`. Suggest `pip install -e 'mcp-server/.[ui]'`.
|
||||
|
||||
4. **On failure**: open `mcp-server/tests/report.html` (pytest-html output, self-contained) and extract the `Meshtastic debug` section for each failed test. That section includes a firmware log stream (last 200 lines) and device state dump. For each failure, summarise:
|
||||
- test name
|
||||
- one-line assertion message
|
||||
- the specific firmware log lines that explain why (look for `PKI_UNKNOWN_PUBKEY`, `Skip send NodeInfo`, `Error=`, `Guru Meditation`, `assertion failed`, `No suitable channel`)
|
||||
- for UI-tier failures also check `mcp-server/tests/ui_captures/<session>/<test>/transcript.md` (per-step frame + OCR)
|
||||
|
||||
5. **Classify each failure** as one of:
|
||||
- **Transient flake** — LoRa collision, first-attempt NAK with self-heal pattern, timing-sensitive assertion. Suggest `/mcp-repro <test-id>` to confirm.
|
||||
- **Environmental** — device unreachable, port busy, CP2102 driver wedged on macOS. Suggest recovery in escalation order: (a) replug USB, (b) `touch_1200bps` + `pio_flash` for nRF52 DFU, (c) `uhubctl_cycle(role=..., confirm=True)` for a device wedged past DFU (needs `uhubctl` installed; `baked_single` does this once automatically when available). Also check `git status userPrefs.jsonc`.
|
||||
- **Regression** — same assertion fails repeatedly on re-runs, firmware log shows novel errors. Identify the firmware module likely responsible.
|
||||
|
||||
6. **Do NOT run destructive recovery automatically**. If a failure looks like it needs a reflash, factory*reset, `uhubctl_cycle`, or replug — \_describe the steps* and let the operator decide. Never burn airtime or flash cycles without approval.
|
||||
|
||||
## Arguments convention
|
||||
|
||||
Operators generally invoke this prompt either with no arguments (full suite) or with a specific subset. Examples:
|
||||
|
||||
- `tests/mesh` — one tier
|
||||
- `tests/mesh/test_direct_with_ack.py::test_direct_with_ack_roundtrip` — one test
|
||||
- `--force-bake` — reflash devices first
|
||||
- `-k telemetry` — name-filter
|
||||
|
||||
## Side-effects to confirm in your summary
|
||||
|
||||
- `userPrefs.jsonc` should be clean after a successful run. The session fixture in `mcp-server/tests/conftest.py` (`_session_userprefs`) snapshots and restores. Check `git status --porcelain userPrefs.jsonc` and report if it's non-empty.
|
||||
- `mcp-server/tests/report.html` and `junit.xml` regenerate on every run.
|
||||
- The wrapper prints a warning if a `.mcp-session-bak` sidecar was left over from a crashed prior session and auto-restores from it — mention that if it happened.
|
||||
@@ -0,0 +1,138 @@
|
||||
# New Meshtastic Module
|
||||
|
||||
Guide for developing a new Meshtastic firmware module.
|
||||
|
||||
## Module Hierarchy
|
||||
|
||||
Choose the appropriate base class:
|
||||
|
||||
1. **`MeshModule`** — Raw base class. Override `wantPacket()` and `handleReceived()`. Returns `ProcessMessage::STOP` or `ProcessMessage::CONTINUE`.
|
||||
2. **`SinglePortModule`** — Handles a single `meshtastic_PortNum`. Constructor takes `(name, portNum)`. Simplified `wantPacket()` checking `decoded.portnum`. Use `allocDataPacket()` to create outgoing packets.
|
||||
3. **`ProtobufModule<T>`** — Template for protobuf-encoded modules. Constructor takes `(name, portNum, fields)`. Override `handleReceivedProtobuf()`. Use `allocDataProtobuf(payload)` to create outgoing packets.
|
||||
|
||||
Most modules also mix in `concurrency::OSThread` for periodic background tasks.
|
||||
|
||||
## Implementation Pattern
|
||||
|
||||
```cpp
|
||||
// src/modules/MyModule.h
|
||||
#pragma once
|
||||
#include "ProtobufModule.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
|
||||
class MyModule : public ProtobufModule<meshtastic_MyMessage>, private concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
MyModule();
|
||||
|
||||
protected:
|
||||
// Process incoming protobuf packet. Return true to stop further processing.
|
||||
virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_MyMessage *msg) override;
|
||||
|
||||
// Generate response packet (optional)
|
||||
virtual meshtastic_MeshPacket *allocReply() override;
|
||||
|
||||
// Periodic task — return next run interval in ms, or disable()
|
||||
virtual int32_t runOnce() override;
|
||||
|
||||
// Modify packet in-flight before delivery (optional)
|
||||
virtual bool alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_MyMessage *msg);
|
||||
|
||||
// Request a UI display frame (optional)
|
||||
virtual bool wantUIFrame();
|
||||
};
|
||||
```
|
||||
|
||||
## Registration
|
||||
|
||||
Register in `src/modules/Modules.cpp` inside `setupModules()`:
|
||||
|
||||
```cpp
|
||||
#if !MESHTASTIC_EXCLUDE_MYMODULE
|
||||
new MyModule();
|
||||
#endif
|
||||
```
|
||||
|
||||
If other code needs to reference the module instance:
|
||||
|
||||
```cpp
|
||||
#if !MESHTASTIC_EXCLUDE_MYMODULE
|
||||
myModule = new MyModule();
|
||||
#endif
|
||||
```
|
||||
|
||||
And declare the global in the header:
|
||||
|
||||
```cpp
|
||||
extern MyModule *myModule;
|
||||
```
|
||||
|
||||
Some modules also conditionally instantiate based on `moduleConfig`:
|
||||
|
||||
```cpp
|
||||
#if !MESHTASTIC_EXCLUDE_MYMODULE
|
||||
if (moduleConfig.has_my_module && moduleConfig.my_module.enabled) {
|
||||
new MyModule();
|
||||
}
|
||||
#endif
|
||||
```
|
||||
|
||||
## Conditional Compilation
|
||||
|
||||
Add a `MESHTASTIC_EXCLUDE_MYMODULE` guard. This allows the module to be excluded from constrained builds. The flag name must follow the pattern: `MESHTASTIC_EXCLUDE_` + uppercase module name.
|
||||
|
||||
## Protobuf Messages (if needed)
|
||||
|
||||
1. Define messages in `protobufs/meshtastic/` (e.g., `mymodule.proto`)
|
||||
2. Add a `.options` file for nanopb field size constraints
|
||||
3. Regenerate with `bin/regen-protos.sh`
|
||||
4. Generated code appears in `src/mesh/generated/meshtastic/`
|
||||
5. Assign a `meshtastic_PortNum` if the module uses a new port number
|
||||
|
||||
## Timing and Defaults
|
||||
|
||||
Use `Default` class helpers for configurable intervals:
|
||||
|
||||
```cpp
|
||||
int32_t MyModule::runOnce()
|
||||
{
|
||||
uint32_t interval = Default::getConfiguredOrDefaultMs(moduleConfig.my_module.update_interval,
|
||||
default_my_module_interval);
|
||||
// ... do work ...
|
||||
return interval;
|
||||
}
|
||||
```
|
||||
|
||||
On public/default channels, enforce minimums with `Default::getConfiguredOrMinimumValue()`.
|
||||
|
||||
## Observer Pattern
|
||||
|
||||
Subscribe to system events:
|
||||
|
||||
```cpp
|
||||
CallbackObserver<MyModule, const meshtastic::Status *> statusObserver =
|
||||
CallbackObserver<MyModule, const meshtastic::Status *>(this, &MyModule::handleStatusUpdate);
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Add test suite in `test/test_mymodule/`:
|
||||
|
||||
```text
|
||||
test/
|
||||
└── test_mymodule/
|
||||
└── test_main.cpp
|
||||
```
|
||||
|
||||
Run with: `pio test -e native`
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Header and implementation files in `src/modules/`
|
||||
- [ ] Inherit from appropriate base class (MeshModule / SinglePortModule / ProtobufModule)
|
||||
- [ ] Mix in OSThread if periodic work is needed
|
||||
- [ ] Register in `src/modules/Modules.cpp` with `MESHTASTIC_EXCLUDE_` guard
|
||||
- [ ] Add protobuf definitions if needed (`protobufs/meshtastic/`)
|
||||
- [ ] Use `Default::getConfiguredOrDefaultMs()` for timing
|
||||
- [ ] Respect bandwidth limits on public channels
|
||||
- [ ] Add test suite in `test/`
|
||||
@@ -0,0 +1,149 @@
|
||||
# New Telemetry Sensor
|
||||
|
||||
Guide for adding a new I2C telemetry sensor driver to Meshtastic firmware.
|
||||
|
||||
## Overview
|
||||
|
||||
Telemetry sensors live in `src/modules/Telemetry/Sensor/`. There are 50+ existing drivers organized by measurement type. Each sensor integrates with one of the telemetry modules:
|
||||
|
||||
- **EnvironmentTelemetryModule** — Temperature, humidity, pressure, gas, light
|
||||
- **AirQualityTelemetryModule** — Particulate matter, VOCs
|
||||
- **PowerTelemetryModule** — Voltage, current, power monitoring
|
||||
- **HealthTelemetryModule** — Heart rate, SpO2, body temperature
|
||||
|
||||
## Sensor Driver Pattern
|
||||
|
||||
Each sensor has a `.h` and `.cpp` file pair following this pattern:
|
||||
|
||||
```cpp
|
||||
// src/modules/Telemetry/Sensor/MySensor.h
|
||||
#pragma once
|
||||
#include "TelemetrySensor.h"
|
||||
#include <MySensorLibrary.h> // Arduino/PlatformIO library
|
||||
|
||||
class MySensor : virtual public TelemetrySensor
|
||||
{
|
||||
private:
|
||||
MySensorLibrary sensor;
|
||||
|
||||
public:
|
||||
MySensor() : TelemetrySensor(meshtastic_TelemetrySensorType_MY_SENSOR, "MySensor") {}
|
||||
|
||||
// Initialize sensor hardware. Return true on success.
|
||||
virtual void setup() override;
|
||||
|
||||
// Read sensor data into the telemetry protobuf. Return true on success.
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
};
|
||||
```
|
||||
|
||||
```cpp
|
||||
// src/modules/Telemetry/Sensor/MySensor.cpp
|
||||
#include "MySensor.h"
|
||||
#include "TelemetrySensor.h"
|
||||
|
||||
void MySensor::setup()
|
||||
{
|
||||
sensor.begin();
|
||||
// Configure sensor parameters...
|
||||
}
|
||||
|
||||
bool MySensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
// Read from hardware
|
||||
float value = sensor.readValue();
|
||||
|
||||
// Populate the appropriate protobuf variant
|
||||
measurement->variant.environment_metrics.temperature = value;
|
||||
// ... other fields ...
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
## I2C Address Registration
|
||||
|
||||
Register the sensor's I2C address(es) in `src/detect/ScanI2C` so it's auto-detected at boot:
|
||||
|
||||
1. Add a `DeviceType` enum entry in `src/detect/ScanI2C.h`
|
||||
2. Add the I2C address mapping in `src/detect/ScanI2CTwoWire.cpp`
|
||||
|
||||
The scan runs at boot and populates a device map that telemetry modules use to decide which sensors to initialize.
|
||||
|
||||
## Protobuf Fields
|
||||
|
||||
If the sensor provides data not covered by existing telemetry fields:
|
||||
|
||||
1. Add fields to the appropriate message in `protobufs/meshtastic/telemetry.proto`:
|
||||
- `EnvironmentMetrics` — Environmental measurements
|
||||
- `AirQualityMetrics` — Air quality data
|
||||
- `PowerMetrics` — Power/energy data
|
||||
- `HealthMetrics` — Health/biometric data
|
||||
2. Add a `.options` constraint if needed (field sizes for nanopb)
|
||||
3. Regenerate: `bin/regen-protos.sh`
|
||||
|
||||
## Sensor Type Enum
|
||||
|
||||
Add the sensor to `meshtastic_TelemetrySensorType` enum in `protobufs/meshtastic/telemetry.proto`:
|
||||
|
||||
```protobuf
|
||||
enum TelemetrySensorType {
|
||||
// ... existing entries ...
|
||||
MY_SENSOR = XX;
|
||||
}
|
||||
```
|
||||
|
||||
## Integration with Telemetry Module
|
||||
|
||||
Wire the sensor into the appropriate telemetry module. For environment sensors, this is typically in `src/modules/Telemetry/EnvironmentTelemetry.cpp`:
|
||||
|
||||
1. Include the sensor header
|
||||
2. Add initialization in `setupSensor()` guarded by detection results
|
||||
3. Call `getMetrics()` in the measurement collection path
|
||||
|
||||
Example pattern from existing sensors:
|
||||
|
||||
```cpp
|
||||
#include "Sensor/MySensor.h"
|
||||
|
||||
MySensor mySensor;
|
||||
|
||||
// In setup:
|
||||
if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MY_SENSOR].first > 0) {
|
||||
mySensor.setup();
|
||||
}
|
||||
|
||||
// In measurement collection:
|
||||
if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MY_SENSOR].first > 0) {
|
||||
mySensor.getMetrics(&measurement);
|
||||
}
|
||||
```
|
||||
|
||||
## Library Dependencies
|
||||
|
||||
If the sensor needs an external library, add it to the `lib_deps` in the relevant base platformio.ini configs:
|
||||
|
||||
```ini
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
mysensorlibrary@^1.0.0
|
||||
```
|
||||
|
||||
Or use a conditional dependency if it's platform-specific.
|
||||
|
||||
## Unit Conversions
|
||||
|
||||
If the sensor reports values in non-standard units, use `src/modules/Telemetry/UnitConversions.h` for conversion helpers (e.g., Celsius ↔ Fahrenheit, hPa ↔ inHg).
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Create `src/modules/Telemetry/Sensor/MySensor.h` and `.cpp`
|
||||
- [ ] Inherit from `TelemetrySensor` base class
|
||||
- [ ] Implement `setup()` and `getMetrics()` methods
|
||||
- [ ] Add `meshtastic_TelemetrySensorType` enum entry in `telemetry.proto`
|
||||
- [ ] Add I2C address to `src/detect/ScanI2C` for auto-detection
|
||||
- [ ] Add protobuf fields in `telemetry.proto` if new data types needed
|
||||
- [ ] Regenerate protos: `bin/regen-protos.sh`
|
||||
- [ ] Wire into the appropriate telemetry module (Environment/AirQuality/Power/Health)
|
||||
- [ ] Add library dependency if external library required
|
||||
- [ ] Test on hardware or native build
|
||||
@@ -0,0 +1,178 @@
|
||||
# New Hardware Variant
|
||||
|
||||
Guide for adding a new Meshtastic hardware variant to the firmware.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
Create under `variants/<arch>/<name>/`:
|
||||
|
||||
```text
|
||||
variants/
|
||||
├── esp32/ # ESP32
|
||||
├── esp32s3/ # ESP32-S3
|
||||
├── esp32c3/ # ESP32-C3
|
||||
├── esp32c6/ # ESP32-C6
|
||||
├── nrf52840/ # nRF52840
|
||||
├── rp2040/ # RP2040/RP2350
|
||||
├── stm32/ # STM32WL
|
||||
└── native/ # Linux/Portduino
|
||||
```
|
||||
|
||||
Each variant needs at minimum:
|
||||
|
||||
- `variant.h` — Pin definitions and hardware capabilities
|
||||
- `platformio.ini` — Build configuration
|
||||
|
||||
Optional files:
|
||||
|
||||
- `pins_arduino.h` — Arduino pin mapping overrides
|
||||
- `rfswitch.h` — RF switch control for multi-band radios
|
||||
- `nicheGraphics.h` — InkHUD e-ink configuration
|
||||
|
||||
## variant.h Template
|
||||
|
||||
```cpp
|
||||
// Pin definitions
|
||||
#define I2C_SDA 21
|
||||
#define I2C_SCL 22
|
||||
|
||||
// LoRa radio
|
||||
#define USE_SX1262 // Radio chip: USE_SX1262, USE_SX1268, USE_SX1280, USE_RF95, USE_LLCC68, USE_LR1110, USE_LR1120, USE_LR1121
|
||||
#define LORA_CS 18
|
||||
#define LORA_SCK 5
|
||||
#define LORA_MOSI 27
|
||||
#define LORA_MISO 19
|
||||
#define LORA_DIO1 33 // SX126x: DIO1, SX128x: DIO1, RF95: IRQ
|
||||
#define LORA_RESET 23
|
||||
#define LORA_BUSY 32 // SX126x/SX128x only
|
||||
#define SX126X_DIO2_AS_RF_SWITCH // Common for SX1262 boards
|
||||
|
||||
// GPS
|
||||
#define HAS_GPS 1
|
||||
#define GPS_RX_PIN 34
|
||||
#define GPS_TX_PIN 12
|
||||
// #define PIN_GPS_EN 47 // Optional GPS enable pin
|
||||
// #define GPS_BAUDRATE 9600 // Override default 9600
|
||||
|
||||
// Display
|
||||
#define HAS_SCREEN 1
|
||||
// #define USE_SSD1306 // OLED type
|
||||
// #define USE_SH1106 // Alternative OLED
|
||||
// #define USE_ST7789 // TFT type
|
||||
// #define SCREEN_WIDTH 128
|
||||
// #define SCREEN_HEIGHT 64
|
||||
|
||||
// LEDs
|
||||
#define LED_PIN 2 // Status LED (optional)
|
||||
// #define HAS_NEOPIXEL 1 // WS2812 support
|
||||
|
||||
// Buttons
|
||||
#define BUTTON_PIN 38
|
||||
// #define BUTTON_PIN_ALT 0 // Secondary button
|
||||
|
||||
// Power management
|
||||
// #define HAS_AXP192 1 // AXP192 PMU (T-Beam v1.0)
|
||||
// #define HAS_AXP2101 1 // AXP2101 PMU (T-Beam v1.2+)
|
||||
// #define BATTERY_PIN 35 // ADC battery voltage pin
|
||||
// #define ADC_MULTIPLIER 2.0 // Voltage divider ratio
|
||||
|
||||
// Optional I2C devices
|
||||
// #define HAS_RTC 1 // Real-time clock
|
||||
// #define HAS_TELEMETRY 1 // Enable telemetry sensor support
|
||||
// #define HAS_SENSOR 1 // I2C sensors present
|
||||
```
|
||||
|
||||
## platformio.ini Template
|
||||
|
||||
```ini
|
||||
[env:my_variant]
|
||||
extends = esp32s3_base ; Use architecture-specific base
|
||||
board = esp32-s3-devkitc-1 ; PlatformIO board definition (or custom in boards/)
|
||||
board_level = extra ; Build level: extra, or omit for default
|
||||
custom_meshtastic_support_level = 1 ; 1 = PR builds, 2 = merge builds only
|
||||
|
||||
build_flags =
|
||||
${esp32s3_base.build_flags}
|
||||
-D MY_VARIANT_SPECIFIC_FLAG=1
|
||||
-I variants/esp32s3/my_variant ; Include path for variant.h
|
||||
|
||||
upload_speed = 921600
|
||||
```
|
||||
|
||||
### Common Base Configs
|
||||
|
||||
- `esp32_base` / `esp32-common.ini` — ESP32
|
||||
- `esp32s3_base` — ESP32-S3
|
||||
- `esp32c3_base` — ESP32-C3
|
||||
- `esp32c6_base` — ESP32-C6
|
||||
- `nrf52840_base` / `nrf52.ini` — nRF52840
|
||||
- `rp2040_base` — RP2040/RP2350
|
||||
|
||||
### Support Levels
|
||||
|
||||
- `custom_meshtastic_support_level = 1` — Built on every PR (actively supported)
|
||||
- `custom_meshtastic_support_level = 2` — Built only on merge to main branches
|
||||
- `board_level = extra` — Only built on full releases
|
||||
|
||||
## Build Manifest Metadata
|
||||
|
||||
`bin/platformio-custom.py` emits UI capability flags in the build manifest:
|
||||
|
||||
- `custom_meshtastic_has_mui = true/false` — Override MUI detection
|
||||
- `custom_meshtastic_has_ink_hud = true/false` — Override InkHUD detection
|
||||
- Architecture names are normalized (e.g., `esp32s3` → `esp32-s3`)
|
||||
|
||||
## InkHUD E-Ink Variants
|
||||
|
||||
For e-ink display variants using the InkHUD framework, add `nicheGraphics.h`:
|
||||
|
||||
```cpp
|
||||
// nicheGraphics.h — InkHUD configuration for this variant
|
||||
#define INKHUD // Enable InkHUD
|
||||
// Configure display, applets, and refresh behavior per device
|
||||
```
|
||||
|
||||
InkHUD has its own PlatformIO config: `src/graphics/niche/InkHUD/PlatformioConfig.ini`
|
||||
|
||||
## I2C Device Detection
|
||||
|
||||
If the variant has I2C devices, ensure `src/detect/ScanI2C` will detect them. The auto-detection system handles 80+ device types including displays, sensors, RTCs, keyboards, PMUs, and touch controllers at boot.
|
||||
|
||||
## Custom Board Definitions
|
||||
|
||||
If the PlatformIO board doesn't exist, create a custom board JSON in `boards/`:
|
||||
|
||||
```json
|
||||
{
|
||||
"build": {
|
||||
"arduino": { "ldscript": "esp32s3_out.ld" },
|
||||
"core": "esp32",
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"flash_mode": "qio",
|
||||
"mcu": "esp32s3",
|
||||
"variant": "esp32s3"
|
||||
},
|
||||
"connectivity": ["wifi", "bluetooth"],
|
||||
"frameworks": ["arduino", "espidf"],
|
||||
"name": "My Custom Board",
|
||||
"upload": {
|
||||
"flash_size": "8MB",
|
||||
"maximum_ram_size": 327680,
|
||||
"maximum_size": 8388608
|
||||
},
|
||||
"url": "https://example.com",
|
||||
"vendor": "MyVendor"
|
||||
}
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Create `variants/<arch>/<name>/variant.h` with pin definitions
|
||||
- [ ] Create `variants/<arch>/<name>/platformio.ini` extending correct base
|
||||
- [ ] Set `custom_meshtastic_support_level` (1 or 2)
|
||||
- [ ] Verify radio chip define matches hardware (`USE_SX1262`, etc.)
|
||||
- [ ] Set hardware capability flags (`HAS_GPS`, `HAS_SCREEN`, etc.)
|
||||
- [ ] Add custom board JSON in `boards/` if needed
|
||||
- [ ] Test build: `pio run -e my_variant`
|
||||
- [ ] For e-ink: add `nicheGraphics.h` with InkHUD config
|
||||
@@ -32,10 +32,15 @@ jobs:
|
||||
shell: bash
|
||||
working-directory: meshtasticd
|
||||
run: |
|
||||
# Build-tools (notably platformio) come from the Meshtastic project
|
||||
# on the OpenSUSE Build Service:
|
||||
# https://build.opensuse.org/project/show/network:Meshtastic:build-tools
|
||||
echo 'deb http://download.opensuse.org/repositories/network:/Meshtastic:/build-tools/xUbuntu_24.04/ /' \
|
||||
| sudo tee /etc/apt/sources.list.d/network:Meshtastic:build-tools.list
|
||||
curl -fsSL https://download.opensuse.org/repositories/network:Meshtastic:build-tools/xUbuntu_24.04/Release.key \
|
||||
| gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/network_Meshtastic_build-tools.gpg >/dev/null
|
||||
sudo apt-get update -y --fix-missing
|
||||
sudo apt-get install -y software-properties-common build-essential devscripts equivs
|
||||
sudo add-apt-repository ppa:meshtastic/build-tools -y
|
||||
sudo apt-get update -y --fix-missing
|
||||
sudo apt-get install -y build-essential devscripts equivs
|
||||
sudo mk-build-deps --install --remove --tool='apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends --yes' debian/control
|
||||
|
||||
- name: Import GPG key
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
name: Build MacOS Binary
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
macos_ver:
|
||||
required: false
|
||||
default: "26" # ARM64
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-MacOS:
|
||||
runs-on: macos-${{ inputs.macos_ver }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install deps
|
||||
shell: bash
|
||||
run: |
|
||||
brew update
|
||||
brew install platformio yaml-cpp libuv openssl@3 libusb argp-standalone pkg-config ulfius
|
||||
|
||||
- name: Get release version string
|
||||
run: |
|
||||
echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
|
||||
id: version
|
||||
|
||||
- name: Build for MacOS
|
||||
run: |
|
||||
platformio run -e native-macos
|
||||
env:
|
||||
PKG_VERSION: ${{ steps.version.outputs.long }}
|
||||
# Errors in this step should not fail the entire workflow while MacOS support is in development.
|
||||
continue-on-error: true
|
||||
|
||||
- name: List output files
|
||||
run: ls -lah .pio/build/native-macos/
|
||||
|
||||
- name: Store binaries as an artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: firmware-macos-${{ inputs.macos_ver }}-${{ steps.version.outputs.long }}
|
||||
overwrite: true
|
||||
path: |
|
||||
.pio/build/native-macos/meshtasticd
|
||||
@@ -73,7 +73,9 @@ jobs:
|
||||
- name: Sanitize platform string
|
||||
id: sanitize_platform
|
||||
# Replace slashes with underscores
|
||||
run: echo "cleaned_platform=${{ inputs.platform }}" | sed 's/\//_/g' >> $GITHUB_OUTPUT
|
||||
env:
|
||||
plat: ${{ inputs.platform }}
|
||||
run: echo "cleaned_platform=${plat}" | sed 's/\//_/g' >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Docker login
|
||||
if: ${{ inputs.push }}
|
||||
|
||||
@@ -43,6 +43,15 @@ jobs:
|
||||
push: true
|
||||
secrets: inherit
|
||||
|
||||
docker-debian-riscv64:
|
||||
uses: ./.github/workflows/docker_build.yml
|
||||
with:
|
||||
distro: debian
|
||||
platform: linux/riscv64
|
||||
runs-on: ubuntu-24.04-arm
|
||||
push: true
|
||||
secrets: inherit
|
||||
|
||||
docker-alpine-amd64:
|
||||
uses: ./.github/workflows/docker_build.yml
|
||||
with:
|
||||
@@ -70,16 +79,27 @@ jobs:
|
||||
push: true
|
||||
secrets: inherit
|
||||
|
||||
docker-alpine-riscv64:
|
||||
uses: ./.github/workflows/docker_build.yml
|
||||
with:
|
||||
distro: alpine
|
||||
platform: linux/riscv64
|
||||
runs-on: ubuntu-24.04-arm
|
||||
push: true
|
||||
secrets: inherit
|
||||
|
||||
docker-manifest:
|
||||
needs:
|
||||
# Debian
|
||||
- docker-debian-amd64
|
||||
- docker-debian-arm64
|
||||
- docker-debian-armv7
|
||||
- docker-debian-riscv64
|
||||
# Alpine
|
||||
- docker-alpine-amd64
|
||||
- docker-alpine-arm64
|
||||
- docker-alpine-armv7
|
||||
- docker-alpine-riscv64
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -162,6 +182,7 @@ jobs:
|
||||
meshtastic/meshtasticd@${{ needs.docker-debian-amd64.outputs.digest }}
|
||||
meshtastic/meshtasticd@${{ needs.docker-debian-arm64.outputs.digest }}
|
||||
meshtastic/meshtasticd@${{ needs.docker-debian-armv7.outputs.digest }}
|
||||
meshtastic/meshtasticd@${{ needs.docker-debian-riscv64.outputs.digest }}
|
||||
|
||||
- name: Docker meta (Alpine)
|
||||
id: meta_alpine
|
||||
@@ -182,3 +203,4 @@ jobs:
|
||||
meshtastic/meshtasticd@${{ needs.docker-alpine-amd64.outputs.digest }}
|
||||
meshtastic/meshtasticd@${{ needs.docker-alpine-arm64.outputs.digest }}
|
||||
meshtastic/meshtasticd@${{ needs.docker-alpine-armv7.outputs.digest }}
|
||||
meshtastic/meshtasticd@${{ needs.docker-alpine-riscv64.outputs.digest }}
|
||||
|
||||
@@ -116,6 +116,20 @@ jobs:
|
||||
build_location: local
|
||||
secrets: inherit
|
||||
|
||||
MacOS:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
macos_ver:
|
||||
- "26" # ARM64
|
||||
# - '26-intel' # x86_64
|
||||
- "15" # ARM64
|
||||
# - '15-intel' # x86_64
|
||||
uses: ./.github/workflows/build_macos_bin.yml
|
||||
with:
|
||||
macos_ver: ${{ matrix.macos_ver }}
|
||||
# secrets: inherit
|
||||
|
||||
package-pio-deps-native-tft:
|
||||
if: ${{ github.repository == 'meshtastic/firmware' && github.event_name == 'workflow_dispatch' }}
|
||||
uses: ./.github/workflows/package_pio_deps.yml
|
||||
@@ -286,6 +300,7 @@ jobs:
|
||||
- gather-artifacts
|
||||
- build-debian-src
|
||||
- package-pio-deps-native-tft
|
||||
# - MacOS
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
@@ -301,10 +316,12 @@ jobs:
|
||||
id: release_notes
|
||||
run: |
|
||||
chmod +x ./bin/generate_release_notes.py
|
||||
NOTES=$(./bin/generate_release_notes.py ${{ needs.version.outputs.long }})
|
||||
NOTES=$(./bin/generate_release_notes.py ${{ needs.version.outputs.long }} --compare-ref HEAD 2>release_notes.log)
|
||||
echo "notes<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$NOTES" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
echo "### Release note range" >> $GITHUB_STEP_SUMMARY
|
||||
cat release_notes.log >> $GITHUB_STEP_SUMMARY
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -466,7 +483,7 @@ jobs:
|
||||
- name: Generate release notes
|
||||
run: |
|
||||
chmod +x ./bin/generate_release_notes.py
|
||||
./bin/generate_release_notes.py ${{ needs.version.outputs.long }} > ./publish/release_notes.md
|
||||
./bin/generate_release_notes.py ${{ needs.version.outputs.long }} --compare-ref HEAD > ./publish/release_notes.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
@@ -86,7 +86,13 @@ jobs:
|
||||
run: sed -i 's/-DBUILD_EPOCH=$UNIX_TIME/#-DBUILD_EPOCH=$UNIX_TIME/' platformio.ini
|
||||
|
||||
- name: PlatformIO Tests
|
||||
run: platformio test -e coverage -v --junit-output-path testreport.xml
|
||||
run: |
|
||||
set -o pipefail
|
||||
# Filter out SKIPPED summary rows for hardware variants that can't run on the
|
||||
# native host. They flood the log and make it harder to spot real failures.
|
||||
# The JUnit XML is written directly to testreport.xml before the pipe, so
|
||||
# the test artifact is unaffected.
|
||||
platformio test -e coverage -v --junit-output-path testreport.xml 2>&1 | grep -v "[[:space:]]SKIPPED$"
|
||||
|
||||
- name: Save test results
|
||||
if: always() # run this step even if previous step failed
|
||||
|
||||
+15
@@ -54,3 +54,18 @@ CMakeLists.txt
|
||||
|
||||
# PYTHONPATH used by the Nix shell
|
||||
.python3
|
||||
.claude/scheduled_tasks.lock
|
||||
userPrefs.jsonc.mcp-session-bak
|
||||
|
||||
# Fake-NodeDB fixture pipeline (bin/regen-fake-nodedbs.sh)
|
||||
# JSONL seeds are committed (test/fixtures/nodedb/seed_v25_*.jsonl);
|
||||
# compiled .proto outputs are ephemeral build artifacts.
|
||||
build/fixtures/
|
||||
bin/_generated/
|
||||
|
||||
# Build artifacts: anything compiling .o/.a outside .pio/ is accidental.
|
||||
# Explicit exceptions for vendored binaries (Morse Micro mm-iot-esp32 SDK).
|
||||
*.o
|
||||
*.a
|
||||
!lib/MorseWlan/lib/**/*.a
|
||||
!lib/MorseWlan/src/*.mbin.o
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"meshtastic": {
|
||||
"command": "./mcp-server/.venv/bin/python",
|
||||
"args": ["-m", "meshtastic_mcp"],
|
||||
"env": {
|
||||
"MESHTASTIC_FIRMWARE_ROOT": "."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
-1
@@ -1,2 +1,28 @@
|
||||
[bandit]
|
||||
skips = B101
|
||||
# Rule IDs: https://bandit.readthedocs.io/en/latest/plugins/index.html
|
||||
#
|
||||
# B101 assert_used
|
||||
# pytest assertions + internal invariants; required for pytest.
|
||||
# B110 try_except_pass
|
||||
# best-effort cleanup paths (atexit handlers, pubsub unsubscribe,
|
||||
# session-end file close, socket shutdown). Logging inside the
|
||||
# except block would be worse than the silent pass — teardown is
|
||||
# already at end-of-session and the surrounding caller has context.
|
||||
# B112 try_except_continue
|
||||
# defensive loops over flaky sources (pubsub handlers, device
|
||||
# re-enumeration polls). One failed iteration shouldn't abort the loop.
|
||||
# B404 import_subprocess
|
||||
# mcp-server wraps PlatformIO, esptool, nrfutil, picotool, and the
|
||||
# pytest test-runner — subprocess is a load-bearing import here, not
|
||||
# a smell. The "consider possible security implications" advisory is
|
||||
# redundant given the file-level review already applied.
|
||||
# B603 subprocess_without_shell_equals_true
|
||||
# all subprocess calls use a static argv list; `shell=False` is the
|
||||
# default and we never string-interpolate user input into the command.
|
||||
# B606 start_process_with_no_shell
|
||||
# same invariant as B603 — running a binary via argv list (not
|
||||
# `shell=True`) is the safe pattern bandit is asking for.
|
||||
#
|
||||
# Higher-severity checks (B102 exec_used, B301 pickle, B307 eval,
|
||||
# B602 shell=True, etc.) remain enabled.
|
||||
skips = B101,B110,B112,B404,B603,B606
|
||||
+16
-9
@@ -4,22 +4,22 @@ cli:
|
||||
plugins:
|
||||
sources:
|
||||
- id: trunk
|
||||
ref: v1.7.6
|
||||
ref: v1.9.0
|
||||
uri: https://github.com/trunk-io/plugins
|
||||
lint:
|
||||
enabled:
|
||||
- checkov@3.2.517
|
||||
- renovate@43.110.9
|
||||
- prettier@3.8.1
|
||||
- trufflehog@3.94.3
|
||||
- checkov@3.2.528
|
||||
- renovate@43.150.0
|
||||
- prettier@3.8.3
|
||||
- trufflehog@3.95.3
|
||||
- yamllint@1.38.0
|
||||
- bandit@1.9.4
|
||||
- trivy@0.69.3
|
||||
- trivy@0.70.0
|
||||
- taplo@0.10.0
|
||||
- ruff@0.15.9
|
||||
- ruff@0.15.12
|
||||
- isort@8.0.1
|
||||
- markdownlint@0.48.0
|
||||
- oxipng@10.1.0
|
||||
- oxipng@10.1.1
|
||||
- svgo@4.0.1
|
||||
- actionlint@1.7.12
|
||||
- flake8@7.3.0
|
||||
@@ -34,9 +34,16 @@ lint:
|
||||
- linters: [ALL]
|
||||
paths:
|
||||
- bin/**
|
||||
# Fake-NodeDB fixture JSONL files contain deterministic synthetic
|
||||
# public_key_hex (64-char hex) values that gitleaks misidentifies as
|
||||
# generic-api-key. These are not secrets — they're test fixtures
|
||||
# produced by bin/gen-fake-nodedb-seed.py with a fixed RNG seed.
|
||||
- linters: [gitleaks]
|
||||
paths:
|
||||
- test/fixtures/nodedb/seed_v25_*.jsonl
|
||||
runtimes:
|
||||
enabled:
|
||||
- python@3.10.8
|
||||
- python@3.14.4
|
||||
- go@1.21.0
|
||||
- node@22.16.0
|
||||
actions:
|
||||
|
||||
Vendored
+4
-4
@@ -1,10 +1,10 @@
|
||||
{
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"platformio.platformio-ide"
|
||||
"Jason2866.esp-decoder",
|
||||
"pioarduino.pioarduino-ide"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
"ms-vscode.cpptools-extension-pack"
|
||||
"ms-vscode.cpptools-extension-pack",
|
||||
"platformio.platformio-ide"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
# Agent instructions
|
||||
|
||||
This repository is the [Meshtastic](https://meshtastic.org) firmware — a C++17 embedded codebase targeting ESP32 / nRF52 / RP2040 / STM32WL / Linux-Portduino LoRa mesh radios — plus a Python MCP server in `mcp-server/` that AI agents use to flash, configure, and test connected devices.
|
||||
|
||||
## Primary instruction file
|
||||
|
||||
**Read `.github/copilot-instructions.md` first.** That file is the canonical agent-facing document for this repo. It covers project layout, coding conventions (naming, module framework, Observer pattern, thread safety), the build system, CI/CD, the native C++ test suite, and — most importantly for automation work — the **MCP Server & Hardware Test Harness** section. Read it top-to-bottom before starting any non-trivial change.
|
||||
|
||||
This file (`AGENTS.md`) is a short pointer + quick reference for agents that don't read `.github/copilot-instructions.md` by default.
|
||||
|
||||
## Quick command reference
|
||||
|
||||
| Action | Command |
|
||||
| -------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
||||
| Build a firmware variant | `pio run -e <env>` (e.g. `pio run -e rak4631`, `pio run -e heltec-v3`) |
|
||||
| Build native macOS host binary | `pio run -e native-macos` (Homebrew prereqs + CH341 LoRa setup in `variants/native/portduino/platformio.ini`) |
|
||||
| Clean + rebuild | `pio run -e <env> -t clean && pio run -e <env>` |
|
||||
| Flash a device | `pio run -e <env> -t upload --upload-port <port>` (or use the `pio_flash` MCP tool) |
|
||||
| Run firmware unit tests (native) | `pio test -e native` |
|
||||
| Run MCP hardware tests | `./mcp-server/run-tests.sh` |
|
||||
| Live TUI test runner | `mcp-server/.venv/bin/meshtastic-mcp-test-tui` |
|
||||
| Format before commit | `trunk fmt` |
|
||||
| Regenerate protobuf bindings | `bin/regen-protos.sh` |
|
||||
| Generate CI matrix | `./bin/generate_ci_matrix.py all [--level pr]` |
|
||||
|
||||
## MCP server (device + test automation)
|
||||
|
||||
The `mcp-server/` package exposes ~32 MCP tools for device discovery, building, flashing, serial monitoring, and live-node administration. Tools are grouped as:
|
||||
|
||||
- **Discovery**: `list_devices`, `list_boards`, `get_board`
|
||||
- **Build & flash**: `build`, `clean`, `pio_flash`, `erase_and_flash` (ESP32 factory), `update_flash` (ESP32 OTA), `touch_1200bps`
|
||||
- **Serial sessions**: `serial_open`, `serial_read`, `serial_list`, `serial_close`
|
||||
- **Device reads**: `device_info`, `list_nodes`
|
||||
- **Device writes** (require `confirm=True`): `set_owner`, `get_config`, `set_config`, `get_channel_url`, `set_channel_url`, `send_text`, `reboot`, `shutdown`, `factory_reset`, `set_debug_log_api`
|
||||
- **userPrefs admin**: `userprefs_get`, `userprefs_set`, `userprefs_reset`, `userprefs_manifest`, `userprefs_testing_profile`
|
||||
- **Vendor escape hatches**: `esptool_*`, `nrfutil_*`, `picotool_*`
|
||||
|
||||
Setup: `cd mcp-server && python3 -m venv .venv && .venv/bin/pip install -e '.[test]'`. The repo registers the server via `.mcp.json` — Claude Code picks it up automatically.
|
||||
|
||||
See `mcp-server/README.md` for argument shapes and the **MCP Server & Hardware Test Harness** section of `.github/copilot-instructions.md` for agent usage rules (tool surface, fixture contract, firmware integration points, recovery playbooks).
|
||||
|
||||
## Slash commands (AI-assisted workflows)
|
||||
|
||||
Three test-and-diagnose workflows exist as slash commands:
|
||||
|
||||
- **`/test` (Claude Code) / `/mcp-test` (Copilot)** — run the hardware test suite and interpret failures
|
||||
- **`/diagnose` / `/mcp-diagnose`** — read-only device health report
|
||||
- **`/repro` / `/mcp-repro`** — flakiness triage: re-run one test N times, diff firmware logs between passes and failures
|
||||
|
||||
Bodies live in `.claude/commands/` and `.github/prompts/` respectively. `.claude/commands/README.md` is the index.
|
||||
|
||||
## Encryption at a glance
|
||||
|
||||
Two layers, both in `src/mesh/CryptoEngine.cpp`:
|
||||
|
||||
- **Channel (symmetric)** — **AES-CTR** with a channel-wide PSK (AES-128 or AES-256). Nonce = packet_id ‖ from_node ‖ block_counter. No AEAD; integrity is soft (channel-hash filter). The well-known default PSK lives in `src/mesh/Channels.h`; a 1-byte PSK is a short-form index into it.
|
||||
- **Per-peer PKI** — **X25519 ECDH** (Curve25519, 32-byte keys) → SHA-256 → **AES-256-CCM** with an 8-byte MAC. Fresh 32-bit `extraNonce` per packet, sent in the clear alongside the MAC. 12-byte wire overhead (`MESHTASTIC_PKC_OVERHEAD`). Used for DMs. Also used for remote admin (`src/modules/AdminModule.cpp`), where AdminMessage authorization is gated by `config.security.admin_key[0..2]`. Disabled entirely in Ham mode (`user.is_licensed=true`).
|
||||
|
||||
Key rotation to never trigger casually: only the **full** factory reset (`factory_reset_device`, `eraseBleBonds=true`) wipes `security.private_key` and regenerates the keypair — every peer holds the old public key, so DMs silently fail PKI decrypt until NodeInfo re-exchanges. The **partial** config reset (`factory_reset_config`) preserves the private key and doesn't invalidate peer relationships. Explicitly blanking `security.private_key` via admin also triggers regen. See the **Encryption & Key Management** section of `.github/copilot-instructions.md` for the full spec (nonce layout, send/receive selection logic including infrastructure-portnum exceptions, admin-key + session-passkey authorization, `is_managed` scope, key-rotation hazards).
|
||||
|
||||
## House rules
|
||||
|
||||
- **No destructive device operations without operator approval.** `factory_reset`, `erase_and_flash`, `reboot`, `shutdown`, history-rewriting git ops — describe the action and stop. Operator authorizes.
|
||||
- **One MCP call per serial port at a time.** The port lock is exclusive; concurrent calls deadlock. Sequence: open → read/mutate → close, then next device.
|
||||
- **`userPrefs.jsonc` is session state during tests.** The `_session_userprefs` fixture snapshots + restores it; never edit it from inside a test.
|
||||
- **Don't speculate about firmware root causes.** When evidence doesn't support a classification, say "unknown" and list what would disambiguate.
|
||||
- **Run `trunk fmt` before proposing a commit.** The `trunk_check` CI gate will reject unformatted code.
|
||||
- **`confirm=True` on destructive MCP tools is a real gate, not a formality.** Don't bypass it via auto-approve settings.
|
||||
- **Keep code comments minimal — one or two lines, max.** Comment only when the _why_ isn't obvious from the code; never restate what the next line does. No multi-paragraph block comments explaining straightforward changes. The diff and commit message carry the rationale; the code carries the behavior.
|
||||
- **Use `Throttle` for time-based rate limiting, not raw `millis()` math.** `src/mesh/Throttle.h` provides `Throttle::isWithinTimespanMs(lastMs, intervalMs)` (returns true while inside the cooldown) and `Throttle::execute(&lastMs, intervalMs, func)` (function-pointer form that updates the timestamp on fire). Use these for any "did N ms pass since X" check — raw `millis() > lastMs + N` is rollover-unsafe (breaks after ~49.7 days) and inconsistent with the rest of the codebase. The helpers compute `now - lastMs` with unsigned subtraction, which wraps correctly.
|
||||
|
||||
## Typical agent workflows
|
||||
|
||||
### Flashing a device
|
||||
|
||||
1. `list_devices` → find the port + likely VID
|
||||
2. `list_boards` → confirm the env, or use the known default for the hardware
|
||||
3. `pio_flash(env=..., port=..., confirm=True)` for any arch, or `erase_and_flash(env=..., port=..., confirm=True)` for an ESP32 factory install
|
||||
|
||||
### Inspecting live node state
|
||||
|
||||
1. `device_info(port=...)` — short summary (node num, firmware version, region, peer count)
|
||||
2. `list_nodes(port=...)` — full peer table (SNR, RSSI, pubkey presence, last_heard)
|
||||
3. `get_config(section="lora", port=...)` — LoRa settings for cross-device comparison
|
||||
|
||||
Sequence these; don't parallelize on the same port.
|
||||
|
||||
### Testing a firmware change
|
||||
|
||||
1. Build locally: `pio run -e <env>`
|
||||
2. Flash the test device: `pio_flash(env=..., port=..., confirm=True)`
|
||||
3. Run the suite: `./mcp-server/run-tests.sh tests/<tier>` or `/test tests/<tier>`
|
||||
4. On failure, open `mcp-server/tests/report.html` → `Meshtastic debug` section for the firmware log tail + device state dump
|
||||
5. Iterate
|
||||
|
||||
### Debugging a flaky test
|
||||
|
||||
1. `/repro <test-node-id> [count]` — re-runs the test N times, diffs firmware logs between passes and failures
|
||||
2. If the first attempt always fails and the rest pass, that's a state-leak pattern → suggest `--force-bake` or a clean device state, don't chase the first failure
|
||||
3. If all N fail, this isn't a flake — it's a regression. Stop iterating and escalate to `/test` for full-suite context.
|
||||
|
||||
## Where to look
|
||||
|
||||
| Path | What's there |
|
||||
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `src/` | Firmware C++ source (`mesh/`, `modules/`, `platform/`, `graphics/`, `gps/`, `motion/`, `mqtt/`, …) |
|
||||
| `src/mesh/` | Core: NodeDB, Router, Channels, CryptoEngine, radio interfaces, StreamAPI, PhoneAPI |
|
||||
| `src/modules/` | Feature modules; `Telemetry/Sensor/` has 50+ I2C sensor drivers |
|
||||
| `variants/` | 200+ hardware variant definitions (`variant.h` + `platformio.ini` per board) |
|
||||
| `protobufs/` | `.proto` definitions; regenerate with `bin/regen-protos.sh` |
|
||||
| `test/` | Firmware unit tests (12 suites; `pio test -e native`) |
|
||||
| `mcp-server/` | Python MCP server + pytest hardware integration tests |
|
||||
| `mcp-server/tests/` | Tiered pytest suite: `unit/`, `mesh/`, `telemetry/`, `monitor/`, `recovery/`, `ui/`, `fleet/`, `admin/`, `provisioning/` |
|
||||
| `.claude/commands/` | Claude Code slash command bodies |
|
||||
| `.github/prompts/` | Copilot prompt bodies (mirrors of the Claude Code ones) |
|
||||
| `.github/copilot-instructions.md` | **Primary agent instructions — read this** |
|
||||
| `.github/workflows/` | CI pipelines |
|
||||
| `.mcp.json` | MCP server registration for Claude Code |
|
||||
|
||||
## Recovery one-liners
|
||||
|
||||
- **`userPrefs.jsonc` dirty after a test run?** Re-run `./mcp-server/run-tests.sh` once (pre-flight self-heals from the sidecar). If still dirty: `git checkout userPrefs.jsonc`.
|
||||
- **nRF52 not responding?** `mcp__meshtastic__touch_1200bps(port=...)` drops it into the DFU bootloader, then `pio_flash` re-installs.
|
||||
- **Device fully wedged (no DFU)?** `mcp__meshtastic__uhubctl_cycle(role="nrf52", confirm=True)` hard-power-cycles it via USB hub PPPS. Needs `uhubctl` installed (`brew install uhubctl` / `apt install uhubctl`); on Linux without udev rules, permission errors fail fast, so use `sudo uhubctl` yourself or configure udev access.
|
||||
- **Port busy?** `lsof <port>` to find the holder. Usually a stale `pio device monitor` or zombie `meshtastic_mcp` process. Kill it.
|
||||
- **Multiple MCP servers running?** `ps aux | grep meshtastic_mcp` — zombies hold ports. Kill all but the one your host spawned.
|
||||
- **macOS: `LIBUSB_ERROR_BUSY` on a CH341 LoRa adapter?** A third-party WCH `CH34xVCPDriver` is claiming interface 0. Find the bundle ID with `ioreg -p IOUSB -l -w 0 | grep -B2 -A30 0x5512`, then `sudo kmutil unload -b <bundleID>`. Apple's bundled CH34x kext targets the CH340 UART (PID 0x7523), not the SPI bridge — it's never the culprit.
|
||||
|
||||
## Environment variables (test harness)
|
||||
|
||||
| Var | Purpose |
|
||||
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `MESHTASTIC_MCP_ENV_<ROLE>` | Override PlatformIO env for a role (e.g. `MESHTASTIC_MCP_ENV_NRF52=rak4631-dap`). Default map: `nrf52→rak4631`, `esp32s3→heltec-v3`. |
|
||||
| `MESHTASTIC_MCP_SEED` | PSK seed for the session test profile. Defaults to `mcp-<user>-<host>`. |
|
||||
| `MESHTASTIC_MCP_FLASH_LOG` | File path to tee pio/esptool/nrfutil/picotool output. `run-tests.sh` sets this to `tests/flash.log` so the TUI can stream live flash progress. |
|
||||
| `MESHTASTIC_MCP_TCP_HOST` | `host` or `host:port` of a `meshtasticd` daemon (e.g. the `native-macos` build). Surfaces it in `list_devices` as `tcp://host:port` so `connect()`-based tools target it transparently. Default port 4403. |
|
||||
| `MESHTASTIC_UHUBCTL_BIN` | Absolute path to `uhubctl` binary. Default: PATH lookup. |
|
||||
| `MESHTASTIC_UHUBCTL_LOCATION_<ROLE>` | Pin a role to a specific uhubctl hub location (e.g. `1-1.3`). Wins over VID auto-detection — use when multiple devices share a VID. |
|
||||
| `MESHTASTIC_UHUBCTL_PORT_<ROLE>` | Pin a role to a specific hub port number. Required alongside `LOCATION_<ROLE>`. |
|
||||
| `MESHTASTIC_UI_CAMERA_BACKEND` | Camera backend for UI tier + `capture_screen` tool: `opencv` / `ffmpeg` / `null` / `auto` (default). |
|
||||
| `MESHTASTIC_UI_CAMERA_DEVICE` | Generic camera device (index or path). Used by the UI tier when no per-role var is set. |
|
||||
| `MESHTASTIC_UI_CAMERA_DEVICE_<ROLE>` | Per-role camera pinning (e.g. `MESHTASTIC_UI_CAMERA_DEVICE_ESP32S3=0` for the OLED-bearing heltec-v3). |
|
||||
| `MESHTASTIC_UI_OCR_BACKEND` | OCR engine selection: `easyocr` / `pytesseract` / `null` / `auto` (default). |
|
||||
| `MESHTASTIC_UI_TUI_CAMERA` | Set to `1` to mount the live camera-feed panel in `meshtastic-mcp-test-tui`. |
|
||||
+3
-1
@@ -3,15 +3,17 @@
|
||||
# trunk-ignore-all(hadolint/DL3008): Do not pin apt package versions
|
||||
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
|
||||
|
||||
FROM python:3.14-slim-trixie AS builder
|
||||
FROM debian:trixie AS builder
|
||||
ARG PIO_ENV=native
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV TZ=Etc/UTC
|
||||
|
||||
# Install Dependencies
|
||||
ENV PIP_ROOT_USER_ACTION=ignore
|
||||
ENV PIP_BREAK_SYSTEM_PACKAGES=1
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||
curl wget g++ zip git ca-certificates pkg-config \
|
||||
python3-pip python3-grpc-tools \
|
||||
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
|
||||
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
|
||||
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \
|
||||
|
||||
+10
-3
@@ -3,12 +3,19 @@
|
||||
# trunk-ignore-all(hadolint/DL3018): Do not pin apk package versions
|
||||
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
|
||||
|
||||
FROM python:3.14-alpine3.22 AS builder
|
||||
# Ensure the Alpine version is updated in both stages of the container!
|
||||
FROM alpine:3.23 AS builder
|
||||
ARG PIO_ENV=native
|
||||
ENV PIP_ROOT_USER_ACTION=ignore
|
||||
|
||||
# Enable Alpine community repository (for 'py3-grpcio-tools')
|
||||
RUN echo "https://dl-cdn.alpinelinux.org/alpine/v$(cut -d. -f1,2 /etc/alpine-release)/community" >> /etc/apk/repositories
|
||||
|
||||
# Install Dependencies
|
||||
ENV PIP_ROOT_USER_ACTION=ignore
|
||||
ENV PIP_BREAK_SYSTEM_PACKAGES=1
|
||||
RUN apk --no-cache add \
|
||||
bash g++ libstdc++-dev linux-headers zip git ca-certificates libbsd-dev \
|
||||
py3-pip py3-grpcio-tools \
|
||||
libgpiod-dev yaml-cpp-dev bluez-dev \
|
||||
libusb-dev i2c-tools-dev libuv-dev openssl-dev pkgconf argp-standalone \
|
||||
libx11-dev libinput-dev libxkbcommon-dev sqlite-dev sdl2-dev \
|
||||
@@ -60,4 +67,4 @@ EXPOSE 4403
|
||||
|
||||
CMD [ "sh", "-cx", "meshtasticd --fsdir=/var/lib/meshtasticd" ]
|
||||
|
||||
HEALTHCHECK NONE
|
||||
HEALTHCHECK NONE
|
||||
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Post-process protoc-generated Python files to live under a local namespace.
|
||||
|
||||
Called by bin/regen-py-protos.sh. Walks the generated *_pb2.py files in the
|
||||
target directory and rewrites every `meshtastic` reference (imports, dotted
|
||||
attribute access) to use the new namespace (e.g., `meshtastic_v25`).
|
||||
|
||||
Why: the .proto files declare `package meshtastic;`, so protoc emits
|
||||
`from meshtastic import mesh_pb2 as ...` lines. That would shadow the PyPI
|
||||
`meshtastic` package which other parts of the mcp-server depend on. Renaming
|
||||
to a local namespace keeps both available.
|
||||
|
||||
Usage:
|
||||
_rewrite_proto_namespace.py <generated_dir> <new_namespace>
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
def rewrite(dir_path: pathlib.Path, new_ns: str) -> int:
|
||||
# Standard protoc import forms:
|
||||
# from meshtastic.X_pb2 import ... (rare, for direct symbol pulls)
|
||||
# from meshtastic import X_pb2 as ... (common, the cross-file ref)
|
||||
# import meshtastic.X_pb2 (also possible)
|
||||
pattern_dotted_from = re.compile(r"^from meshtastic\.", re.MULTILINE)
|
||||
pattern_bare_from = re.compile(r"^from meshtastic import ", re.MULTILINE)
|
||||
pattern_dotted_import = re.compile(r"^import meshtastic\.", re.MULTILINE)
|
||||
|
||||
count = 0
|
||||
for p in dir_path.glob("*.py"):
|
||||
text = p.read_text(encoding="utf-8")
|
||||
new = pattern_dotted_from.sub(f"from {new_ns}.", text)
|
||||
new = pattern_bare_from.sub(f"from {new_ns} import ", new)
|
||||
new = pattern_dotted_import.sub(f"import {new_ns}.", new)
|
||||
# NOTE: we deliberately leave `meshtastic/X.proto` source-filename
|
||||
# references inside descriptor strings alone. The descriptor pool is
|
||||
# keyed by source filename (independent of Python package layout), so
|
||||
# those don't collide with the PyPI package's descriptors.
|
||||
if new != text:
|
||||
p.write_text(new, encoding="utf-8")
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
if len(argv) != 2:
|
||||
print("usage: _rewrite_proto_namespace.py <generated_dir> <new_namespace>", file=sys.stderr)
|
||||
return 2
|
||||
dir_path = pathlib.Path(argv[0])
|
||||
new_ns = argv[1]
|
||||
if not dir_path.is_dir():
|
||||
print(f"directory not found: {dir_path}", file=sys.stderr)
|
||||
return 2
|
||||
n = rewrite(dir_path, new_ns)
|
||||
print(f"rewrote {n} file(s) in {dir_path} → namespace {new_ns}", file=sys.stderr)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
+2
-1
@@ -31,5 +31,6 @@ basename=meshtasticd-$1-$VERSION
|
||||
pio pkg install --environment "$PIO_ENV" || platformioFailed
|
||||
pio run --environment "$PIO_ENV" || platformioFailed
|
||||
|
||||
cp "$BUILDDIR/meshtasticd" "$OUTDIR/meshtasticd_linux_$(uname -m)"
|
||||
os_name=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
cp "$BUILDDIR/meshtasticd" "$OUTDIR/meshtasticd_${os_name}_$(uname -m)"
|
||||
cp bin/native-install.* $OUTDIR/
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# For use with Armbian luckfox-pico-max
|
||||
# Waveshare LoRa HAT for Raspberry Pi Pico
|
||||
# https://www.waveshare.com/wiki/Pico-LoRa-SX1262
|
||||
|
||||
Meta:
|
||||
name: luckfox-pico-max-ws-raspberry-pi-pico-hat
|
||||
support: community
|
||||
compatible:
|
||||
- luckfox-pico-max # Armbian
|
||||
|
||||
Lora:
|
||||
Module: sx1262
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
spidev: spidev0.0
|
||||
Busy: # GPIO1_C7 / GP2
|
||||
pin: 55
|
||||
gpiochip: 1
|
||||
line: 23
|
||||
CS: # GPIO1_C6 / GP3
|
||||
pin: 54
|
||||
gpiochip: 1
|
||||
line: 22
|
||||
Reset: # GPIO1_D1 / GP15
|
||||
pin: 57
|
||||
gpiochip: 1
|
||||
line: 25
|
||||
IRQ: # GPIO2_A2 / GP20
|
||||
pin: 66
|
||||
gpiochip: 2
|
||||
line: 2
|
||||
@@ -0,0 +1,18 @@
|
||||
# Station G3 motherboard with a Raspberry Pi Zero 2W as the MCU daughterboard.
|
||||
# Verify spidev / I2C device paths for your OS — they may differ.
|
||||
Meta:
|
||||
name: Station G3
|
||||
support: community
|
||||
compatible:
|
||||
- raspberry-pi
|
||||
|
||||
Lora:
|
||||
Module: sx1262
|
||||
IRQ: 22 # BCM pin — wiki spec
|
||||
Reset: 16 # BCM pin — wiki spec
|
||||
Busy: 24 # BCM pin — wiki spec
|
||||
# CS: 8 # BCM 8 = SPI0 CE0 (default); uncomment only to override
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
spidev: spidev0.0
|
||||
# SX126X_MAX_POWER: 19 # matches Station G2 firmware cap; raise carefully per PA jumper mode
|
||||
Executable
+439
@@ -0,0 +1,439 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Deterministic seed-data generator for the fake NodeDB fixture pipeline.
|
||||
|
||||
Writes a JSONL file describing N fake-but-realistic Meshtastic peers.
|
||||
The output is hand-editable and committed; a sibling compile step
|
||||
(bin/seed-json-to-proto.py) turns it into a binary `meshtastic_NodeDatabase`
|
||||
v25 protobuf with fresh "now-relative" timestamps.
|
||||
|
||||
Determinism contract:
|
||||
Same --seed -> byte-identical JSONL output, regardless of wall clock.
|
||||
All timestamps are stored as `*_offset_sec` (seconds before "now"); the
|
||||
compile step resolves them to absolute epochs at compile time.
|
||||
|
||||
Structural fields covered:
|
||||
* NodeInfoLite header: num, long_name, short_name, hw_model, role,
|
||||
public_key, snr, channel, hops_away, next_hop, bitfield flags
|
||||
* PositionLite: lat/long Gaussian around --centroid, altitude, source
|
||||
* DeviceMetrics: battery/voltage/util/uptime
|
||||
* EnvironmentMetrics: temp/humidity/pressure/iaq
|
||||
* StatusMessage: error_code (usually zero)
|
||||
|
||||
Active-board allow-list:
|
||||
hw_model values are restricted to the intersection of
|
||||
(a) variants with `custom_meshtastic_support_level = 1` in
|
||||
variants/*/*/platformio.ini, AND
|
||||
(b) values present in the `HardwareModel` enum in mesh.proto.
|
||||
See HW_MODEL_WEIGHTS below. Deprecated boards (legacy TLORA / Heltec V1-2 /
|
||||
classic TBEAM / TBEAM_V0P7 / Nano G1 / etc.) and fuzzer-only sentinels
|
||||
(PORTDUINO, ANDROID_SIM, DIY_V1, ...) are excluded.
|
||||
|
||||
Active-role allow-list:
|
||||
Excludes ROUTER_CLIENT (deprecated v2.3.15) and REPEATER (deprecated v2.7.11).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime as _dt
|
||||
import json
|
||||
import math
|
||||
import pathlib
|
||||
import random
|
||||
import sys
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Active-board allow-list (intersection of tier-1 variants + HardwareModel enum).
|
||||
# Refresh by running:
|
||||
# for f in $(find variants -name 'platformio.ini' | xargs grep -lE 'custom_meshtastic_support_level = 1'); do
|
||||
# grep custom_meshtastic_hw_model_slug $f | awk -F= '{print $2}' | tr -d ' ';
|
||||
# done | sort -u | comm -12 - <(python3 -c "from meshtastic.protobuf.mesh_pb2 import HardwareModel; print('\\n'.join(HardwareModel.keys()))" | sort)
|
||||
# --------------------------------------------------------------------------
|
||||
HW_MODEL_WEIGHTS: dict[str, float] = {
|
||||
"HELTEC_V3": 14.0,
|
||||
"T_DECK": 9.0,
|
||||
"HELTEC_V4": 8.0,
|
||||
"RAK4631": 8.0,
|
||||
"HELTEC_MESH_POCKET": 6.0,
|
||||
"TRACKER_T1000_E": 5.0,
|
||||
"HELTEC_MESH_NODE_T114": 5.0,
|
||||
"T_DECK_PRO": 5.0,
|
||||
"LILYGO_TBEAM_S3_CORE": 4.0,
|
||||
"HELTEC_WIRELESS_PAPER": 4.0,
|
||||
"HELTEC_WSL_V3": 3.0,
|
||||
"T_ECHO": 3.0,
|
||||
"HELTEC_WIRELESS_TRACKER": 3.0,
|
||||
"HELTEC_WIRELESS_TRACKER_V2": 2.0,
|
||||
"HELTEC_VISION_MASTER_E290": 2.0,
|
||||
"HELTEC_MESH_SOLAR": 2.0,
|
||||
"SEEED_WIO_TRACKER_L1": 2.0,
|
||||
"T_LORA_PAGER": 1.5,
|
||||
"HELTEC_VISION_MASTER_E213": 1.5,
|
||||
"T_ECHO_PLUS": 1.0,
|
||||
"MUZI_BASE": 1.0,
|
||||
"WISMESH_TAP_V2": 1.0,
|
||||
"THINKNODE_M2": 1.0,
|
||||
"THINKNODE_M5": 1.0,
|
||||
"TLORA_T3_S3": 1.0,
|
||||
# Long tail (uniform low weight across remaining tier-1 boards):
|
||||
"HELTEC_V4_R8": 0.3,
|
||||
"HELTEC_VISION_MASTER_T190": 0.3,
|
||||
"HELTEC_HT62": 0.3,
|
||||
"HELTEC_MESH_NODE_T096": 0.3,
|
||||
"M5STACK_C6L": 0.3,
|
||||
"MINI_EPAPER_S3": 0.3,
|
||||
"MUZI_R1_NEO": 0.3,
|
||||
"NOMADSTAR_METEOR_PRO": 0.3,
|
||||
"RAK3312": 0.3,
|
||||
"RAK3401": 0.3,
|
||||
"SEEED_SOLAR_NODE": 0.3,
|
||||
"SEEED_WIO_TRACKER_L1_EINK": 0.3,
|
||||
"SENSECAP_INDICATOR": 0.3,
|
||||
"TBEAM_1_WATT": 0.3,
|
||||
"THINKNODE_M1": 0.3,
|
||||
"THINKNODE_M3": 0.3,
|
||||
"THINKNODE_M6": 0.3,
|
||||
"T_ECHO_LITE": 0.3,
|
||||
"WISMESH_TAG": 0.3,
|
||||
"WISMESH_TAP": 0.3,
|
||||
"XIAO_NRF52_KIT": 0.3,
|
||||
"CROWPANEL": 0.3,
|
||||
}
|
||||
|
||||
# Non-deprecated roles only.
|
||||
ROLE_WEIGHTS: dict[str, float] = {
|
||||
"CLIENT": 75.0,
|
||||
"CLIENT_MUTE": 5.0,
|
||||
"ROUTER": 7.0,
|
||||
"TRACKER": 3.0,
|
||||
"SENSOR": 2.0,
|
||||
"CLIENT_HIDDEN": 2.0,
|
||||
"ROUTER_LATE": 2.0,
|
||||
"CLIENT_BASE": 2.0,
|
||||
"TAK": 1.0,
|
||||
"TAK_TRACKER": 0.5,
|
||||
"LOST_AND_FOUND": 0.5,
|
||||
}
|
||||
|
||||
# Name pools — 60 firsts × 60 lasts = 3600 combinations.
|
||||
FIRSTS = [
|
||||
"Quick", "Brave", "Silent", "Wild", "Lone", "Bright", "Red", "Blue",
|
||||
"Green", "Black", "White", "Iron", "Steel", "Copper", "Silver", "Gold",
|
||||
"Stone", "River", "Forest", "Mountain", "Canyon", "Desert", "Storm", "Sky",
|
||||
"Solar", "Lunar", "Dawn", "Dusk", "Misty", "Frosty", "Sunny", "Shady",
|
||||
"Happy", "Sleepy", "Drowsy", "Sneaky", "Sharp", "Smooth", "Rough", "Loud",
|
||||
"Soft", "Slow", "Fast", "Tall", "Short", "Old", "New", "Tiny",
|
||||
"Giant", "Hidden", "Lost", "Found", "Wandering", "Roving", "Drifting", "Floating",
|
||||
"Burning", "Frozen", "Whispering", "Howling",
|
||||
]
|
||||
LASTS = [
|
||||
"Phoenix", "Lion", "Bear", "Wolf", "Hawk", "Eagle", "Fox", "Lynx",
|
||||
"Cougar", "Coyote", "Raven", "Owl", "Crow", "Falcon", "Heron", "Crane",
|
||||
"Otter", "Badger", "Bison", "Elk", "Moose", "Stag", "Doe", "Hare",
|
||||
"Marmot", "Mole", "Beaver", "Squirrel", "Mustang", "Bronco", "Pony", "Colt",
|
||||
"Cobra", "Viper", "Mamba", "Adder", "Gecko", "Iguana", "Tortoise", "Turtle",
|
||||
"Salmon", "Trout", "Bass", "Pike", "Shark", "Whale", "Dolphin", "Seal",
|
||||
"Cactus", "Yucca", "Sage", "Juniper", "Pine", "Cedar", "Aspen", "Oak",
|
||||
"Bluff", "Mesa", "Arroyo", "Ridge",
|
||||
]
|
||||
|
||||
# Brief callsign pool for licensed-looking suffixes.
|
||||
CALLSIGN_PREFIXES = ["KX", "WD", "N5", "KE", "AB", "W5", "K1", "KQ", "AE", "NM"]
|
||||
|
||||
# Only emojis that fit in 4 UTF-8 bytes (no variation selectors). short_name's
|
||||
# nanopb max_size:5 (incl. NUL) limits content to 4 bytes. ❄️ / ☀️ would be
|
||||
# 6 bytes due to U+FE0F variation selector — explicitly excluded.
|
||||
EMOJI_SHORTNAMES = ["🦊", "🐺", "🦅", "🐢", "🌵", "🔥", "🌙",
|
||||
"🌊", "🗻", "🌲", "🦌", "🐝", "🦂", "🦉",
|
||||
"🦇", "🦋"]
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
NUM_RESERVED = 4 # firmware reserves 0..3 (per NodeDB constants)
|
||||
NUM_MAX_EXCLUSIVE = 0x80000000 # restrict to positive int32 range for readability
|
||||
|
||||
|
||||
def _weighted_choice(rng: random.Random, weights: dict[str, float]) -> str:
|
||||
"""Deterministic weighted pick. Uses sorted keys so dict order is fixed."""
|
||||
keys = sorted(weights.keys())
|
||||
totals = [weights[k] for k in keys]
|
||||
return rng.choices(keys, weights=totals, k=1)[0]
|
||||
|
||||
|
||||
def _gen_long_name(rng: random.Random, is_licensed: bool) -> str:
|
||||
base = f"{rng.choice(FIRSTS)} {rng.choice(LASTS)}"
|
||||
if is_licensed:
|
||||
prefix = rng.choice(CALLSIGN_PREFIXES)
|
||||
# Two trailing alpha chars after the digit; keep within 25 - len(base) - 1
|
||||
suffix = f" {prefix}{rng.randint(0,9)}{rng.choice('ABCDEFGHIJKLMNOPQRSTUVWXYZ')}{rng.choice('ABCDEFGHIJKLMNOPQRSTUVWXYZ')}"
|
||||
# nanopb max_size:25 means C string fits 24 bytes + NUL.
|
||||
if len(base) + len(suffix) <= 24:
|
||||
base = base + suffix
|
||||
# Hard cap to 24 chars (nanopb max_size:25 minus NUL).
|
||||
return base[:24]
|
||||
|
||||
|
||||
def _gen_short_name(rng: random.Random, long_name: str) -> str:
|
||||
# 10% emoji-only short_name
|
||||
if rng.random() < 0.10:
|
||||
return rng.choice(EMOJI_SHORTNAMES)
|
||||
first_char = long_name[0].upper() if long_name else "X"
|
||||
alphanums = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
return first_char + "".join(rng.choices(alphanums, k=3))
|
||||
|
||||
|
||||
def _gen_hops_away(rng: random.Random) -> int:
|
||||
# Geometric-ish: 0→55%, 1→25%, 2→12%, 3→5%, 4→2%, 5+→1%
|
||||
r = rng.random()
|
||||
if r < 0.55:
|
||||
return 0
|
||||
if r < 0.80:
|
||||
return 1
|
||||
if r < 0.92:
|
||||
return 2
|
||||
if r < 0.97:
|
||||
return 3
|
||||
if r < 0.99:
|
||||
return 4
|
||||
return rng.randint(5, 7)
|
||||
|
||||
|
||||
def _gen_position(
|
||||
rng: random.Random,
|
||||
centroid_lat: float,
|
||||
centroid_lon: float,
|
||||
spread_km: float,
|
||||
last_heard_offset_sec: int,
|
||||
) -> dict:
|
||||
# 1 deg ≈ 111 km at the equator; we use this as a flat approximation.
|
||||
lat = centroid_lat + rng.gauss(0.0, spread_km / 111.0)
|
||||
lon = centroid_lon + rng.gauss(0.0, spread_km / 111.0)
|
||||
altitude = max(0, round(rng.gauss(1376.0, 250.0))) # T or C valley floor + relief
|
||||
# Position was reported up to 300s before last_heard.
|
||||
time_offset_sec = last_heard_offset_sec + rng.randint(0, 300)
|
||||
return {
|
||||
"latitude": round(lat, 6),
|
||||
"longitude": round(lon, 6),
|
||||
"altitude": altitude,
|
||||
"time_offset_sec": time_offset_sec,
|
||||
"location_source": "LOC_INTERNAL",
|
||||
}
|
||||
|
||||
|
||||
def _gen_telemetry(rng: random.Random) -> dict:
|
||||
# 5% plugged-in (battery_level == 101); rest uniform [10..100].
|
||||
if rng.random() < 0.05:
|
||||
battery_level = 101
|
||||
voltage = 4.20
|
||||
else:
|
||||
battery_level = rng.randint(10, 100)
|
||||
voltage = round(3.3 + (battery_level / 100.0) * 0.9, 3)
|
||||
# Beta distributions for low/right-skewed metrics; randomly draw via gammavariate.
|
||||
def _beta(a: float, b: float) -> float:
|
||||
x = rng.gammavariate(a, 1.0)
|
||||
y = rng.gammavariate(b, 1.0)
|
||||
return x / (x + y)
|
||||
channel_utilization = round(_beta(2.0, 15.0) * 100.0, 2)
|
||||
air_util_tx = round(_beta(1.5, 20.0) * 10.0, 3)
|
||||
uptime_seconds = int(rng.expovariate(1.0 / 86400.0))
|
||||
return {
|
||||
"battery_level": battery_level,
|
||||
"voltage": voltage,
|
||||
"channel_utilization": channel_utilization,
|
||||
"air_util_tx": air_util_tx,
|
||||
"uptime_seconds": uptime_seconds,
|
||||
}
|
||||
|
||||
|
||||
def _gen_environment(rng: random.Random) -> dict:
|
||||
return {
|
||||
"temperature": round(rng.gauss(22.0, 8.0), 2),
|
||||
"relative_humidity": round(min(100.0, max(0.0, rng.gauss(55.0, 20.0))), 2),
|
||||
"barometric_pressure": round(rng.gauss(1013.0, 8.0), 2),
|
||||
"iaq": int(min(500, max(0, round(rng.gauss(50.0, 30.0))))),
|
||||
}
|
||||
|
||||
|
||||
def _gen_status(rng: random.Random) -> dict:
|
||||
# `StatusMessage` (mesh.proto:1445) has a single free-form `string status`.
|
||||
# Most peers report a healthy short status; occasional alert string.
|
||||
healthy = ["OK", "online", "active", "running", "ready", "nominal"]
|
||||
alert = ["low-batt", "no-gps", "weak-signal", "rebooted", "offline-soon"]
|
||||
if rng.random() < 0.92:
|
||||
return {"status": rng.choice(healthy)}
|
||||
return {"status": rng.choice(alert)}
|
||||
|
||||
|
||||
def _gen_node(
|
||||
rng: random.Random,
|
||||
num: int,
|
||||
centroid_lat: float,
|
||||
centroid_lon: float,
|
||||
spread_km: float,
|
||||
coverage: dict[str, float],
|
||||
last_heard_mean_sec: int,
|
||||
last_heard_max_sec: int,
|
||||
) -> dict:
|
||||
is_licensed = rng.random() < 0.05
|
||||
long_name = _gen_long_name(rng, is_licensed)
|
||||
short_name = _gen_short_name(rng, long_name)
|
||||
hw_model = _weighted_choice(rng, HW_MODEL_WEIGHTS)
|
||||
role = _weighted_choice(rng, ROLE_WEIGHTS)
|
||||
has_public_key = rng.random() < 0.92
|
||||
public_key_hex = (
|
||||
"".join(f"{rng.randint(0,255):02x}" for _ in range(32)) if has_public_key else ""
|
||||
)
|
||||
snr = round(max(-20.0, min(12.0, rng.gauss(6.0, 4.0))), 2)
|
||||
channel = 0 if rng.random() < 0.90 else rng.randint(1, 7)
|
||||
hops_away = _gen_hops_away(rng)
|
||||
next_hop = rng.randint(0, 255) if hops_away > 0 else 0
|
||||
last_heard_offset_sec = int(min(rng.expovariate(1.0 / last_heard_mean_sec), last_heard_max_sec))
|
||||
|
||||
bitfield = {
|
||||
"has_user": True,
|
||||
"is_favorite": rng.random() < 0.08,
|
||||
"is_muted": rng.random() < 0.03,
|
||||
"via_mqtt": rng.random() < 0.12,
|
||||
"is_ignored": rng.random() < 0.01,
|
||||
"is_licensed": is_licensed,
|
||||
"has_is_unmessagable": True,
|
||||
"is_unmessagable": rng.random() < 0.02,
|
||||
"is_key_manually_verified": rng.random() < 0.04,
|
||||
}
|
||||
|
||||
node: dict = {
|
||||
"num": f"0x{num:08x}",
|
||||
"long_name": long_name,
|
||||
"short_name": short_name,
|
||||
"hw_model": hw_model,
|
||||
"role": role,
|
||||
"public_key_hex": public_key_hex,
|
||||
"snr": snr,
|
||||
"channel": channel,
|
||||
"hops_away": hops_away,
|
||||
"next_hop": next_hop,
|
||||
"last_heard_offset_sec": last_heard_offset_sec,
|
||||
"bitfield": bitfield,
|
||||
"position": (
|
||||
_gen_position(rng, centroid_lat, centroid_lon, spread_km, last_heard_offset_sec)
|
||||
if rng.random() < coverage["position"]
|
||||
else None
|
||||
),
|
||||
"telemetry": _gen_telemetry(rng) if rng.random() < coverage["telemetry"] else None,
|
||||
"environment": _gen_environment(rng) if rng.random() < coverage["environment"] else None,
|
||||
"status": _gen_status(rng) if rng.random() < coverage["status"] else None,
|
||||
}
|
||||
return node
|
||||
|
||||
|
||||
def _parse_my_node_num(s: str | None) -> int | None:
|
||||
if s is None:
|
||||
return None
|
||||
s = s.strip()
|
||||
if s.startswith("0x") or s.startswith("0X"):
|
||||
return int(s, 16)
|
||||
return int(s)
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
p = argparse.ArgumentParser(
|
||||
description="Deterministic JSONL seed for the fake NodeDB fixture.",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
p.add_argument("--count", type=int, required=True, help="Number of fake nodes to emit.")
|
||||
p.add_argument("--seed", type=int, required=True, help="Deterministic seed.")
|
||||
p.add_argument("--out", required=True, help="Output JSONL path.")
|
||||
p.add_argument(
|
||||
"--centroid",
|
||||
default="33.1284,-107.2528",
|
||||
help="LAT,LON centroid (default: Truth or Consequences, NM).",
|
||||
)
|
||||
p.add_argument("--spread-km", type=float, default=60.0, help="Gaussian std-dev in km.")
|
||||
p.add_argument("--position-coverage", type=float, default=0.85)
|
||||
p.add_argument("--telemetry-coverage", type=float, default=0.70)
|
||||
p.add_argument("--environment-coverage", type=float, default=0.25)
|
||||
p.add_argument("--status-coverage", type=float, default=0.40)
|
||||
p.add_argument("--my-node-num", default=None, help="Exclude this NodeNum from generated set (hex or dec).")
|
||||
p.add_argument("--last-heard-mean-sec", type=int, default=3600)
|
||||
p.add_argument("--last-heard-max-sec", type=int, default=7 * 86400)
|
||||
args = p.parse_args(argv)
|
||||
|
||||
if args.count <= 0:
|
||||
print("--count must be positive", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
try:
|
||||
centroid_lat, centroid_lon = (float(s) for s in args.centroid.split(","))
|
||||
except ValueError:
|
||||
print(f"--centroid must be LAT,LON; got {args.centroid!r}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
my_node_num = _parse_my_node_num(args.my_node_num)
|
||||
|
||||
rng = random.Random(args.seed)
|
||||
|
||||
# 1) Generate a unique deterministic set of NodeNums.
|
||||
nums: set[int] = set()
|
||||
while len(nums) < args.count:
|
||||
n = rng.randrange(NUM_RESERVED, NUM_MAX_EXCLUSIVE)
|
||||
if my_node_num is not None and n == my_node_num:
|
||||
continue
|
||||
nums.add(n)
|
||||
ordered_nums = sorted(nums) # sort to fix output order independent of set hash
|
||||
|
||||
# 2) Per-node generation (in num order, single RNG continues).
|
||||
coverage = {
|
||||
"position": args.position_coverage,
|
||||
"telemetry": args.telemetry_coverage,
|
||||
"environment": args.environment_coverage,
|
||||
"status": args.status_coverage,
|
||||
}
|
||||
nodes = [
|
||||
_gen_node(
|
||||
rng,
|
||||
n,
|
||||
centroid_lat,
|
||||
centroid_lon,
|
||||
args.spread_km,
|
||||
coverage,
|
||||
args.last_heard_mean_sec,
|
||||
args.last_heard_max_sec,
|
||||
)
|
||||
for n in ordered_nums
|
||||
]
|
||||
|
||||
# 3) Write JSONL.
|
||||
out_path = pathlib.Path(args.out)
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
# `generated_at_iso` is informational; it does NOT affect determinism because
|
||||
# we derive it from the seed, not from wall clock. (Same seed -> same string.)
|
||||
generated_at = _dt.datetime.fromtimestamp(args.seed, tz=_dt.timezone.utc).isoformat().replace("+00:00", "Z")
|
||||
meta = {
|
||||
"_meta": {
|
||||
"version": 25,
|
||||
"seed": args.seed,
|
||||
"count": args.count,
|
||||
"centroid": [centroid_lat, centroid_lon],
|
||||
"spread_km": args.spread_km,
|
||||
"generated_at_iso": generated_at,
|
||||
"my_node_num_excluded": (None if my_node_num is None else f"0x{my_node_num:08x}"),
|
||||
"coverage": coverage,
|
||||
"last_heard_mean_sec": args.last_heard_mean_sec,
|
||||
"last_heard_max_sec": args.last_heard_max_sec,
|
||||
}
|
||||
}
|
||||
with out_path.open("w", encoding="utf-8") as f:
|
||||
# `ensure_ascii=False` so emoji short_names survive. `sort_keys=True` for
|
||||
# determinism (insertion order varies by Python version otherwise).
|
||||
f.write(json.dumps(meta, ensure_ascii=False, sort_keys=True) + "\n")
|
||||
for node in nodes:
|
||||
f.write(json.dumps(node, ensure_ascii=False, sort_keys=True) + "\n")
|
||||
|
||||
print(f"wrote {args.count} nodes to {out_path} ({out_path.stat().st_size} bytes)", file=sys.stderr)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
@@ -1,25 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate release notes from merged PRs on develop and master branches.
|
||||
Categorizes PRs into Enhancements and Bug Fixes/Maintenance sections.
|
||||
"""
|
||||
"""Generate release notes from the actual release commit range."""
|
||||
|
||||
import subprocess
|
||||
import re
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def get_last_release_tag():
|
||||
"""Get the most recent release tag."""
|
||||
def get_last_release_tag(compare_ref, exclude_tag=None):
|
||||
"""Get the most recent version tag merged into compare_ref."""
|
||||
result = subprocess.run(
|
||||
["git", "describe", "--tags", "--abbrev=0"],
|
||||
["git", "tag", "--merged", compare_ref, "--sort=-version:refname", "v*"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
|
||||
for line in result.stdout.splitlines():
|
||||
candidate = line.strip()
|
||||
if not candidate:
|
||||
continue
|
||||
if exclude_tag and candidate == exclude_tag:
|
||||
continue
|
||||
return candidate
|
||||
|
||||
raise subprocess.CalledProcessError(result.returncode, result.args, output=result.stdout, stderr=result.stderr)
|
||||
|
||||
|
||||
def get_tag_date(tag):
|
||||
@@ -33,18 +39,18 @@ def get_tag_date(tag):
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def get_merged_prs_since_tag(tag, branch):
|
||||
"""Get all merged PRs since the given tag on the specified branch."""
|
||||
# Get commits since tag on the branch - look for PR numbers in parentheses
|
||||
def get_merged_prs_in_range(tag, compare_ref):
|
||||
"""Get all merged PRs in the git range between tag and compare_ref."""
|
||||
result = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"log",
|
||||
f"{tag}..origin/{branch}",
|
||||
f"{tag}..{compare_ref}",
|
||||
"--oneline",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
prs = []
|
||||
@@ -65,6 +71,25 @@ def get_merged_prs_since_tag(tag, branch):
|
||||
return prs
|
||||
|
||||
|
||||
def parse_args():
|
||||
"""Parse CLI arguments."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate release notes from the actual release commit range."
|
||||
)
|
||||
parser.add_argument("new_version", help="Version that will be tagged for this release")
|
||||
parser.add_argument(
|
||||
"--base-tag",
|
||||
dest="base_tag",
|
||||
help="Existing version tag to diff from. Defaults to the latest version tag merged into the compare ref.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--compare-ref",
|
||||
default="HEAD",
|
||||
help="Git ref to diff to. Defaults to HEAD.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def get_pr_details(pr_number):
|
||||
"""Get PR details from GitHub API via gh CLI."""
|
||||
try:
|
||||
@@ -268,28 +293,28 @@ def get_new_contributors(pr_details_list, tag, repo="meshtastic/firmware"):
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: generate_release_notes.py <new_version>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
new_version = sys.argv[1]
|
||||
args = parse_args()
|
||||
new_version = args.new_version
|
||||
compare_ref = args.compare_ref
|
||||
current_tag = f"v{new_version}"
|
||||
|
||||
# Get last release tag
|
||||
try:
|
||||
last_tag = get_last_release_tag()
|
||||
last_tag = args.base_tag or get_last_release_tag(compare_ref, exclude_tag=current_tag)
|
||||
except subprocess.CalledProcessError:
|
||||
print("Error: Could not find last release tag", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Collect PRs from both branches
|
||||
all_pr_numbers = set()
|
||||
print(
|
||||
f"Resolved release note range: {last_tag}..{compare_ref}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
for branch in ["develop", "master"]:
|
||||
try:
|
||||
prs = get_merged_prs_since_tag(last_tag, branch)
|
||||
all_pr_numbers.update(prs)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not get PRs from {branch}: {e}", file=sys.stderr)
|
||||
try:
|
||||
all_pr_numbers = set(get_merged_prs_in_range(last_tag, compare_ref))
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error: Could not get PRs for range {last_tag}..{compare_ref}: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Get details for all PRs
|
||||
enhancements = []
|
||||
|
||||
@@ -87,6 +87,12 @@
|
||||
</screenshots>
|
||||
|
||||
<releases>
|
||||
<release version="2.7.24" date="2026-05-08">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.24</url>
|
||||
</release>
|
||||
<release version="2.7.23" date="2026-04-14">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.23</url>
|
||||
</release>
|
||||
<release version="2.7.22" date="2026-04-06">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.22</url>
|
||||
</release>
|
||||
|
||||
Executable
+73
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regenerate the fake-NodeDB fixtures: produces 250 / 500 / 1000 / 2000-node
|
||||
# JSONL seed files + their compiled v25 protobufs.
|
||||
#
|
||||
# Layout:
|
||||
# test/fixtures/nodedb/seed_v25_<N>.jsonl — COMMITTED, hand-editable.
|
||||
# build/fixtures/nodedb/nodes_v25_<N>.proto — .gitignored, build artifact.
|
||||
# Drop into /prefs/nodes.proto.
|
||||
#
|
||||
# Daily use: ./bin/regen-fake-nodedbs.sh
|
||||
# - Recompiles protos from committed seeds (fresh wall-clock timestamps).
|
||||
# Intentional seed bump: REGEN_SEEDS=yes ./bin/regen-fake-nodedbs.sh
|
||||
# - Overwrites the committed JSONL files with freshly-seeded data.
|
||||
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
# 1) Make sure the Python protobuf bindings exist (in-tree generation; .gitignored).
|
||||
if [[ ! -d bin/_generated/meshtastic ]]; then
|
||||
echo "regenerating Python protobuf bindings (one-time)..."
|
||||
./bin/regen-py-protos.sh
|
||||
fi
|
||||
|
||||
# 2) Pick a Python interpreter that has the meshtastic deps installed.
|
||||
# Prefer the mcp-server venv (most likely to be set up by the operator).
|
||||
PY="python3"
|
||||
for cand in mcp-server/.venv/bin/python3 .venv/bin/python3; do
|
||||
if [[ -x "$cand" ]]; then
|
||||
PY="$cand"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# 3) Pinned seeds per size — bump only when you intentionally want different
|
||||
# structural data committed. Parallel arrays so the script works on
|
||||
# macOS bash 3.2 (no `declare -A`).
|
||||
SIZES=(250 500 1000 2000)
|
||||
SEEDS=(20260511 20260512 20260513 20260514)
|
||||
|
||||
REGEN_SEEDS="${REGEN_SEEDS:-no}"
|
||||
|
||||
mkdir -p build/fixtures/nodedb test/fixtures/nodedb
|
||||
|
||||
for i in 0 1 2 3; do
|
||||
n="${SIZES[$i]}"
|
||||
seed="${SEEDS[$i]}"
|
||||
jsonl=$(printf "test/fixtures/nodedb/seed_v25_%04d.jsonl" "$n")
|
||||
proto=$(printf "build/fixtures/nodedb/nodes_v25_%04d.proto" "$n")
|
||||
|
||||
if [[ "$REGEN_SEEDS" == "yes" || ! -f "$jsonl" ]]; then
|
||||
$PY bin/gen-fake-nodedb-seed.py \
|
||||
--count "$n" \
|
||||
--seed "$seed" \
|
||||
--out "$jsonl" \
|
||||
--centroid 33.1284,-107.2528 \
|
||||
--spread-km 60 \
|
||||
--position-coverage 0.85 \
|
||||
--telemetry-coverage 0.70 \
|
||||
--environment-coverage 0.25 \
|
||||
--status-coverage 0.40
|
||||
echo " seed: $jsonl ($(wc -c < "$jsonl") bytes)"
|
||||
fi
|
||||
|
||||
$PY bin/seed-json-to-proto.py --in "$jsonl" --out "$proto"
|
||||
echo " proto: $proto ($(wc -c < "$proto") bytes)"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Done. To load on Portduino native:"
|
||||
echo " cp build/fixtures/nodedb/nodes_v25_1000.proto ~/.portduino/default/prefs/nodes.proto"
|
||||
echo ""
|
||||
echo "To push to a hardware device:"
|
||||
echo " Use the mcp-server tool: push_fake_nodedb(size=1000, target=\"hardware\", port=\"/dev/cu.usbmodemXXXX\", confirm=True)"
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regenerate Python protobuf bindings from the in-tree `protobufs/` submodule
|
||||
# into `bin/_generated/`. Called by bin/regen-fake-nodedbs.sh; also useful as
|
||||
# a standalone refresh after any change to a .proto file.
|
||||
#
|
||||
# Output is .gitignored — bindings are a build artifact.
|
||||
#
|
||||
# Namespace rewrite:
|
||||
# The .proto files declare `package meshtastic;`, which makes protoc emit
|
||||
# imports like `from meshtastic import mesh_pb2`. That conflicts with the
|
||||
# PyPI `meshtastic` package (which the mcp-server relies on for its
|
||||
# SerialInterface/BLEInterface transport). We post-process the generated
|
||||
# files to live under `meshtastic_v25` instead — both the directory layout
|
||||
# and all internal imports — so they coexist cleanly with the PyPI package.
|
||||
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
if ! command -v protoc >/dev/null 2>&1; then
|
||||
echo "ERROR: protoc not found in PATH." >&2
|
||||
echo " macOS: brew install protobuf" >&2
|
||||
echo " Ubuntu/Debian: apt install protobuf-compiler" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
OUT=bin/_generated
|
||||
LOCAL_NS=meshtastic_v25
|
||||
|
||||
rm -rf "$OUT"
|
||||
mkdir -p "$OUT"
|
||||
|
||||
# 1) Generate from the in-tree protos. nanopb.proto first so its descriptor
|
||||
# is available for the [(nanopb).*] options on other messages.
|
||||
protoc \
|
||||
--proto_path=protobufs \
|
||||
--python_out="$OUT" \
|
||||
protobufs/nanopb.proto \
|
||||
protobufs/meshtastic/*.proto
|
||||
|
||||
# 2) Move the generated `meshtastic/` directory to `meshtastic_v25/`.
|
||||
mv "$OUT/meshtastic" "$OUT/$LOCAL_NS"
|
||||
|
||||
# 3) Rewrite internal imports: any reference to `meshtastic.X_pb2` or
|
||||
# `from meshtastic import X_pb2` becomes `meshtastic_v25.*`.
|
||||
python3 bin/_rewrite_proto_namespace.py "$OUT/$LOCAL_NS" "$LOCAL_NS"
|
||||
|
||||
# 4) Make the package importable.
|
||||
touch "$OUT/__init__.py"
|
||||
touch "$OUT/$LOCAL_NS/__init__.py"
|
||||
|
||||
echo "regenerated Python protobuf bindings -> $OUT/$LOCAL_NS/ (namespace: $LOCAL_NS)" >&2
|
||||
Executable
+342
@@ -0,0 +1,342 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compile a committed seed JSONL into a binary meshtastic_NodeDatabase v25 proto.
|
||||
|
||||
The input is produced by `bin/gen-fake-nodedb-seed.py`. Timestamps in the JSONL
|
||||
are stored as `*_offset_sec` (seconds before "now"); this script resolves them
|
||||
to absolute epochs using `--now-epoch` (default: current wall clock).
|
||||
|
||||
Output is a raw `pb_encode`-compatible binary that can be dropped at
|
||||
`/prefs/nodes.proto` on the device (Portduino prefs dir or hardware via
|
||||
XModem) and loaded by `NodeDB::loadFromDisk` at boot.
|
||||
|
||||
Wire format reference:
|
||||
protobufs/meshtastic/deviceonly.proto (NodeDatabase, NodeInfoLite, sat entries)
|
||||
src/mesh/NodeDB.h:467-484 (bitfield bit positions)
|
||||
src/mesh/NodeDB.cpp:1523-1524 (pb_decode entry point)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
# Prefer the in-tree generated Python protobuf bindings (bin/_generated/meshtastic_v25/)
|
||||
# because the firmware branch's protos (v25 NodeDatabase satellite arrays, slim
|
||||
# NodeInfoLite) are typically newer than what the PyPI `meshtastic` package
|
||||
# ships. Run `bin/regen-py-protos.sh` to (re)generate.
|
||||
#
|
||||
# Namespace note: the local bindings live under `meshtastic_v25` (NOT `meshtastic`)
|
||||
# to avoid shadowing the PyPI `meshtastic` package — bin/regen-py-protos.sh
|
||||
# post-processes the protoc output to rename the package.
|
||||
_HERE = pathlib.Path(__file__).resolve().parent
|
||||
_LOCAL_PROTO_DIR = _HERE / "_generated"
|
||||
if _LOCAL_PROTO_DIR.is_dir():
|
||||
sys.path.insert(0, str(_LOCAL_PROTO_DIR))
|
||||
|
||||
try:
|
||||
from meshtastic_v25.deviceonly_pb2 import ( # type: ignore[import-not-found]
|
||||
NodeDatabase,
|
||||
NodeInfoLite,
|
||||
NodePositionEntry,
|
||||
NodeTelemetryEntry,
|
||||
NodeEnvironmentEntry,
|
||||
NodeStatusEntry,
|
||||
PositionLite,
|
||||
)
|
||||
from meshtastic_v25.mesh_pb2 import HardwareModel, Position, StatusMessage # type: ignore[import-not-found]
|
||||
from meshtastic_v25.config_pb2 import Config # type: ignore[import-not-found]
|
||||
from meshtastic_v25.telemetry_pb2 import DeviceMetrics, EnvironmentMetrics # type: ignore[import-not-found]
|
||||
except ImportError as local_err:
|
||||
# Fall back to the PyPI package if in-tree bindings haven't been generated.
|
||||
# Will fail the v25 assertion below if the PyPI package predates the
|
||||
# satellite-DB schema, but at least gives a clear "run regen-py-protos.sh"
|
||||
# error message instead of an opaque ImportError.
|
||||
try:
|
||||
from meshtastic.protobuf.deviceonly_pb2 import (
|
||||
NodeDatabase,
|
||||
NodeInfoLite,
|
||||
NodePositionEntry,
|
||||
NodeTelemetryEntry,
|
||||
NodeEnvironmentEntry,
|
||||
NodeStatusEntry,
|
||||
PositionLite,
|
||||
)
|
||||
from meshtastic.protobuf.mesh_pb2 import HardwareModel, Position, StatusMessage
|
||||
from meshtastic.protobuf.config_pb2 import Config
|
||||
from meshtastic.protobuf.telemetry_pb2 import DeviceMetrics, EnvironmentMetrics
|
||||
except ImportError as pypi_err:
|
||||
print(
|
||||
"ERROR: could not import meshtastic protobuf bindings.\n"
|
||||
" In-tree generation: run `bin/regen-py-protos.sh` (requires protoc).\n"
|
||||
" PyPI fallback: `pip install meshtastic` (may lag firmware branch).\n"
|
||||
f" local error (meshtastic_v25): {local_err}\n"
|
||||
f" pypi error (meshtastic.protobuf): {pypi_err}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# Fail loudly if bindings predate v25 (no satellite arrays).
|
||||
assert (
|
||||
hasattr(NodeDatabase, "DESCRIPTOR")
|
||||
and "positions" in NodeDatabase.DESCRIPTOR.fields_by_name
|
||||
), (
|
||||
"Loaded meshtastic bindings are older than v25 (NodeDatabase.positions missing). "
|
||||
"Run `bin/regen-py-protos.sh` against the in-tree protobufs/ submodule."
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bitfield bit positions (mirror src/mesh/NodeDB.h:467-484).
|
||||
# ---------------------------------------------------------------------------
|
||||
BIT_IS_KEY_MANUALLY_VERIFIED = 0
|
||||
BIT_IS_MUTED = 1
|
||||
BIT_VIA_MQTT = 2
|
||||
BIT_IS_FAVORITE = 3
|
||||
BIT_IS_IGNORED = 4
|
||||
BIT_HAS_USER = 5
|
||||
BIT_IS_LICENSED = 6
|
||||
BIT_IS_UNMESSAGABLE = 7
|
||||
BIT_HAS_IS_UNMESSAGABLE = 8
|
||||
|
||||
BITFIELD_LAYOUT = (
|
||||
# JSON key bit position
|
||||
("is_key_manually_verified", BIT_IS_KEY_MANUALLY_VERIFIED),
|
||||
("is_muted", BIT_IS_MUTED),
|
||||
("via_mqtt", BIT_VIA_MQTT),
|
||||
("is_favorite", BIT_IS_FAVORITE),
|
||||
("is_ignored", BIT_IS_IGNORED),
|
||||
("has_user", BIT_HAS_USER),
|
||||
("is_licensed", BIT_IS_LICENSED),
|
||||
("is_unmessagable", BIT_IS_UNMESSAGABLE),
|
||||
("has_is_unmessagable", BIT_HAS_IS_UNMESSAGABLE),
|
||||
)
|
||||
|
||||
|
||||
def _pack_bitfield(bf: dict[str, bool]) -> int:
|
||||
out = 0
|
||||
for key, shift in BITFIELD_LAYOUT:
|
||||
if bf.get(key, False):
|
||||
out |= (1 << shift)
|
||||
return out
|
||||
|
||||
|
||||
def _validate_node(node: dict[str, Any]) -> None:
|
||||
"""Friendly errors so hand-editors get clear feedback."""
|
||||
if "num" not in node or not isinstance(node["num"], str):
|
||||
raise ValueError(f"node missing/invalid 'num' (must be hex string): {node!r}")
|
||||
if "long_name" not in node:
|
||||
raise ValueError(f"node {node['num']}: missing 'long_name'")
|
||||
if len(node["long_name"]) > 24:
|
||||
raise ValueError(
|
||||
f"node {node['num']}: long_name {node['long_name']!r} is "
|
||||
f"{len(node['long_name'])} chars; max 24 (nanopb max_size:25 minus NUL)"
|
||||
)
|
||||
if "short_name" in node:
|
||||
# short_name max_size:5 (incl. NUL) → 4 bytes of content.
|
||||
# Char count is irrelevant — emojis with variation selectors (e.g. ❄️ = 6 B)
|
||||
# would slip past a `len(str) > 4` check. Always measure bytes.
|
||||
b = node["short_name"].encode("utf-8")
|
||||
if len(b) > 4:
|
||||
raise ValueError(
|
||||
f"node {node['num']}: short_name {node['short_name']!r} is "
|
||||
f"{len(b)} bytes UTF-8; max 4 (nanopb max_size:5 minus NUL)"
|
||||
)
|
||||
pk = node.get("public_key_hex", "")
|
||||
if pk and len(pk) != 64:
|
||||
raise ValueError(
|
||||
f"node {node['num']}: public_key_hex must be 64 hex chars or empty; "
|
||||
f"got {len(pk)} chars"
|
||||
)
|
||||
if pk:
|
||||
try:
|
||||
bytes.fromhex(pk)
|
||||
except ValueError as e:
|
||||
raise ValueError(f"node {node['num']}: public_key_hex is not valid hex: {e}")
|
||||
|
||||
|
||||
def _resolve_time(
|
||||
node: dict[str, Any],
|
||||
field_absolute: str,
|
||||
field_offset: str,
|
||||
now_epoch: int,
|
||||
) -> int:
|
||||
"""If `field_absolute` is set, use it; else compute `now_epoch - offset`."""
|
||||
if field_absolute in node and node[field_absolute] is not None:
|
||||
return int(node[field_absolute])
|
||||
offset = node.get(field_offset, 0)
|
||||
return max(0, int(now_epoch) - int(offset))
|
||||
|
||||
|
||||
def _build_node_info_lite(node: dict[str, Any], now_epoch: int) -> NodeInfoLite:
|
||||
_validate_node(node)
|
||||
info = NodeInfoLite()
|
||||
info.num = int(node["num"], 16) if isinstance(node["num"], str) else int(node["num"])
|
||||
info.long_name = node.get("long_name", "")
|
||||
info.short_name = node.get("short_name", "")
|
||||
# Enum lookups will raise ValueError on unknown names — that's exactly what we want.
|
||||
info.hw_model = HardwareModel.Value(node.get("hw_model", "UNSET"))
|
||||
info.role = Config.DeviceConfig.Role.Value(node.get("role", "CLIENT"))
|
||||
pk_hex = node.get("public_key_hex", "")
|
||||
if pk_hex:
|
||||
info.public_key = bytes.fromhex(pk_hex)
|
||||
info.snr = float(node.get("snr", 0.0))
|
||||
info.channel = int(node.get("channel", 0))
|
||||
if "hops_away" in node:
|
||||
# `optional uint32 hops_away = 9;` — in Python protobuf, assigning the
|
||||
# field implicitly sets HasField("hops_away") to True. No has_hops_away
|
||||
# setter exists (unlike the C++ nanopb-generated header).
|
||||
info.hops_away = int(node["hops_away"])
|
||||
info.next_hop = int(node.get("next_hop", 0))
|
||||
info.last_heard = _resolve_time(node, "last_heard", "last_heard_offset_sec", now_epoch)
|
||||
info.bitfield = _pack_bitfield(node.get("bitfield", {}))
|
||||
return info
|
||||
|
||||
|
||||
def _build_position_entry(num: int, pos: dict[str, Any], now_epoch: int) -> NodePositionEntry:
|
||||
entry = NodePositionEntry()
|
||||
entry.num = num
|
||||
pl = PositionLite()
|
||||
# Firmware stores lat/long as int32 in 1e-7 degrees.
|
||||
pl.latitude_i = int(round(float(pos["latitude"]) * 1e7))
|
||||
pl.longitude_i = int(round(float(pos["longitude"]) * 1e7))
|
||||
pl.altitude = int(pos.get("altitude", 0))
|
||||
pl.time = _resolve_time(pos, "time", "time_offset_sec", now_epoch)
|
||||
pl.location_source = Position.LocSource.Value(pos.get("location_source", "LOC_UNSET"))
|
||||
entry.position.CopyFrom(pl)
|
||||
return entry
|
||||
|
||||
|
||||
def _build_telemetry_entry(num: int, tel: dict[str, Any]) -> NodeTelemetryEntry:
|
||||
entry = NodeTelemetryEntry()
|
||||
entry.num = num
|
||||
dm = DeviceMetrics()
|
||||
if "battery_level" in tel:
|
||||
dm.battery_level = int(tel["battery_level"])
|
||||
if "voltage" in tel:
|
||||
dm.voltage = float(tel["voltage"])
|
||||
if "channel_utilization" in tel:
|
||||
dm.channel_utilization = float(tel["channel_utilization"])
|
||||
if "air_util_tx" in tel:
|
||||
dm.air_util_tx = float(tel["air_util_tx"])
|
||||
if "uptime_seconds" in tel:
|
||||
dm.uptime_seconds = int(tel["uptime_seconds"])
|
||||
entry.device_metrics.CopyFrom(dm)
|
||||
return entry
|
||||
|
||||
|
||||
def _build_environment_entry(num: int, env: dict[str, Any]) -> NodeEnvironmentEntry:
|
||||
entry = NodeEnvironmentEntry()
|
||||
entry.num = num
|
||||
em = EnvironmentMetrics()
|
||||
if "temperature" in env:
|
||||
em.temperature = float(env["temperature"])
|
||||
if "relative_humidity" in env:
|
||||
em.relative_humidity = float(env["relative_humidity"])
|
||||
if "barometric_pressure" in env:
|
||||
em.barometric_pressure = float(env["barometric_pressure"])
|
||||
if "iaq" in env:
|
||||
em.iaq = int(env["iaq"])
|
||||
entry.environment_metrics.CopyFrom(em)
|
||||
return entry
|
||||
|
||||
|
||||
def _build_status_entry(num: int, status: dict[str, Any]) -> NodeStatusEntry:
|
||||
# `StatusMessage` (mesh.proto:1445) has a single `string status` field.
|
||||
entry = NodeStatusEntry()
|
||||
entry.num = num
|
||||
sm = StatusMessage()
|
||||
if "status" in status:
|
||||
sm.status = str(status["status"])
|
||||
entry.status.CopyFrom(sm)
|
||||
return entry
|
||||
|
||||
|
||||
def compile_jsonl_to_proto(jsonl_path: pathlib.Path, now_epoch: int) -> bytes:
|
||||
"""Read a seed JSONL and return the encoded NodeDatabase bytes."""
|
||||
lines = jsonl_path.read_text(encoding="utf-8").splitlines()
|
||||
if not lines:
|
||||
raise ValueError(f"{jsonl_path} is empty")
|
||||
meta_line = lines[0]
|
||||
meta_obj = json.loads(meta_line)
|
||||
meta = meta_obj.get("_meta", {})
|
||||
version = meta.get("version")
|
||||
if version != 25:
|
||||
raise ValueError(
|
||||
f"{jsonl_path}: meta version is {version!r}; this compiler "
|
||||
f"requires version=25. Regenerate the seed with the matching tooling."
|
||||
)
|
||||
|
||||
db = NodeDatabase()
|
||||
db.version = 25
|
||||
|
||||
for ln, raw in enumerate(lines[1:], start=2):
|
||||
raw = raw.strip()
|
||||
if not raw:
|
||||
continue
|
||||
try:
|
||||
node = json.loads(raw)
|
||||
except json.JSONDecodeError as e:
|
||||
raise ValueError(f"{jsonl_path}:{ln} JSON parse error: {e}")
|
||||
|
||||
num = int(node["num"], 16) if isinstance(node["num"], str) else int(node["num"])
|
||||
|
||||
# Header
|
||||
info = _build_node_info_lite(node, now_epoch)
|
||||
db.nodes.append(info)
|
||||
|
||||
# Satellites (nullable)
|
||||
if node.get("position"):
|
||||
db.positions.append(_build_position_entry(num, node["position"], now_epoch))
|
||||
if node.get("telemetry"):
|
||||
db.telemetry.append(_build_telemetry_entry(num, node["telemetry"]))
|
||||
if node.get("environment"):
|
||||
db.environment.append(_build_environment_entry(num, node["environment"]))
|
||||
if node.get("status"):
|
||||
db.status.append(_build_status_entry(num, node["status"]))
|
||||
|
||||
return db.SerializeToString()
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
p = argparse.ArgumentParser(
|
||||
description="Compile a seed JSONL into a binary v25 NodeDatabase proto.",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
p.add_argument("--in", dest="in_path", required=True, help="Input seed JSONL.")
|
||||
p.add_argument("--out", required=True, help="Output binary .proto path.")
|
||||
p.add_argument(
|
||||
"--now-epoch",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Pin 'now' to this Unix epoch (for byte-identical CI). Default: time.time().",
|
||||
)
|
||||
args = p.parse_args(argv)
|
||||
|
||||
in_path = pathlib.Path(args.in_path)
|
||||
if not in_path.is_file():
|
||||
print(f"input not found: {in_path}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
now_epoch = args.now_epoch if args.now_epoch is not None else int(time.time())
|
||||
|
||||
try:
|
||||
encoded = compile_jsonl_to_proto(in_path, now_epoch)
|
||||
except ValueError as e:
|
||||
print(f"ERROR: {e}", file=sys.stderr)
|
||||
return 3
|
||||
|
||||
out_path = pathlib.Path(args.out)
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_path.write_bytes(encoded)
|
||||
print(
|
||||
f"compiled {in_path} -> {out_path} ({len(encoded)} bytes, now_epoch={now_epoch})",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
Executable
+118
@@ -0,0 +1,118 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to show commits in develop that are not in master
|
||||
# with their associated PR info and commit hashes
|
||||
#
|
||||
# Usage:
|
||||
# ./show-unmerged-prs.sh # Show all unmerged commits
|
||||
# ./show-unmerged-prs.sh --bugfix # Show only bugfix-labeled PRs
|
||||
|
||||
set -e
|
||||
|
||||
REPO="firmware"
|
||||
OWNER="meshtastic"
|
||||
BASE_BRANCH="master"
|
||||
HEAD_BRANCH="develop"
|
||||
LIMIT=100
|
||||
FILTER_LABEL=""
|
||||
|
||||
# Parse arguments
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--bugfix)
|
||||
FILTER_LABEL="bugfix"
|
||||
shift
|
||||
;;
|
||||
--feature)
|
||||
FILTER_LABEL="feature"
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo "Options:"
|
||||
echo " --bugfix Show only PRs labeled with 'bugfix'"
|
||||
echo " --feature Show only PRs labeled with 'feature'"
|
||||
echo " --help Show this help message"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -n "$FILTER_LABEL" ]; then
|
||||
echo "Fetching commits in $HEAD_BRANCH that are not in $BASE_BRANCH (filtered by label: $FILTER_LABEL)..."
|
||||
else
|
||||
echo "Fetching commits in $HEAD_BRANCH that are not in $BASE_BRANCH..."
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Check if gh CLI is available
|
||||
if ! command -v gh &> /dev/null; then
|
||||
echo "ERROR: GitHub CLI (gh) not found. Please install it first."
|
||||
echo "Visit: https://cli.github.com/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get commits in develop that are not in master
|
||||
# For each commit, try to find associated PR
|
||||
git fetch origin develop master 2>/dev/null || true
|
||||
|
||||
# Use git to get the list of commits
|
||||
commits=$(git log --pretty=format:"%H|%s" origin/master..origin/develop | head -n $LIMIT)
|
||||
|
||||
count=0
|
||||
displayed=0
|
||||
echo "Commits in $HEAD_BRANCH not in $BASE_BRANCH:"
|
||||
echo "=============================================="
|
||||
echo ""
|
||||
|
||||
while IFS='|' read -r hash subject; do
|
||||
((count++))
|
||||
|
||||
# Try to find the PR for this commit
|
||||
# Extract PR number, title, description, and labels
|
||||
pr_response=$(gh api -X GET "/repos/$OWNER/$REPO/commits/$hash/pulls" \
|
||||
-H "Accept: application/vnd.github.v3+json" 2>/dev/null | \
|
||||
jq -r '.[0] | "\(.number)|\(.title)|\(.body // "No description")|\(.labels | map(.name) | join(","))"' 2>/dev/null || echo "||||")
|
||||
|
||||
if [ -z "$pr_response" ] || [ "$pr_response" = "||||" ]; then
|
||||
# If no PR found, skip if filter is active, otherwise show the commit
|
||||
if [ -z "$FILTER_LABEL" ]; then
|
||||
((displayed++))
|
||||
echo "[$displayed] Commit: $hash"
|
||||
echo " Subject: $subject"
|
||||
echo " PR: Not found in GitHub"
|
||||
echo ""
|
||||
fi
|
||||
else
|
||||
IFS='|' read -r pr_num pr_title pr_desc pr_labels <<< "$pr_response"
|
||||
|
||||
# Check if filter matches
|
||||
if [ -n "$FILTER_LABEL" ]; then
|
||||
# Only show if the label is in the labels list
|
||||
if ! echo "$pr_labels" | grep -q "$FILTER_LABEL"; then
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
((displayed++))
|
||||
echo "[$displayed] PR #$pr_num - $pr_title"
|
||||
echo " Commit: $hash"
|
||||
if [ -n "$pr_desc" ] && [ "$pr_desc" != "No description" ]; then
|
||||
# Truncate description to 200 chars
|
||||
desc_short="${pr_desc:0:200}"
|
||||
[ ${#pr_desc} -gt 200 ] && desc_short+="..."
|
||||
echo " Description: $desc_short"
|
||||
fi
|
||||
if [ -n "$pr_labels" ] && [ "$pr_labels" != "" ]; then
|
||||
echo " Labels: $pr_labels"
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
done <<< "$commits"
|
||||
|
||||
echo ""
|
||||
if [ -n "$FILTER_LABEL" ]; then
|
||||
echo "Done. Showing $displayed PRs with label '$FILTER_LABEL' from $displayed commits checked."
|
||||
else
|
||||
echo "Done. Showing $displayed commits from $HEAD_BRANCH not in $BASE_BRANCH."
|
||||
fi
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"memory_type": "qio_opi"
|
||||
},
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-D BOARD_HAS_PSRAM",
|
||||
"-D ARDUINO_USB_CDC_ON_BOOT=0",
|
||||
"-D ARDUINO_USB_MODE=0",
|
||||
"-D ARDUINO_RUNNING_CORE=1",
|
||||
"-D ARDUINO_EVENT_RUNNING_CORE=0"
|
||||
],
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"flash_mode": "qio",
|
||||
"psram_type": "qio_opi",
|
||||
"hwids": [["0x303A", "0x1001"]],
|
||||
"mcu": "esp32s3",
|
||||
"variant": "ELECROW-ThinkNode-M7"
|
||||
},
|
||||
"connectivity": ["wifi", "bluetooth", "lora"],
|
||||
"debug": {
|
||||
"default_tool": "esp-builtin",
|
||||
"onboard_tools": ["esp-builtin"],
|
||||
"openocd_target": "esp32s3.cfg"
|
||||
},
|
||||
"frameworks": ["arduino", "espidf"],
|
||||
"name": "ELECROW ThinkNode M7",
|
||||
"upload": {
|
||||
"flash_size": "8MB",
|
||||
"maximum_ram_size": 524288,
|
||||
"maximum_size": 8388608,
|
||||
"use_1200bps_touch": true,
|
||||
"wait_for_upload_port": true,
|
||||
"require_upload_port": true,
|
||||
"speed": 921600
|
||||
},
|
||||
"url": "https://www.elecrow.com",
|
||||
"vendor": "ELECROW"
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"partitions": "default_16MB.csv",
|
||||
"memory_type": "qio_opi"
|
||||
},
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
],
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"flash_mode": "qio",
|
||||
"psram_type": "opi",
|
||||
"hwids": [["0x303A", "0x1001"]],
|
||||
"mcu": "esp32s3",
|
||||
"variant": "heltec_v4_r8"
|
||||
},
|
||||
"connectivity": ["wifi", "bluetooth", "lora"],
|
||||
"debug": {
|
||||
"default_tool": "esp-builtin",
|
||||
"onboard_tools": ["esp-builtin"],
|
||||
"openocd_target": "esp32s3.cfg"
|
||||
},
|
||||
"frameworks": ["arduino", "espidf"],
|
||||
"name": "heltec_wifi_lora_32 v4 r8 (16 MB FLASH, 8 MB PSRAM)",
|
||||
"upload": {
|
||||
"flash_size": "16MB",
|
||||
"maximum_ram_size": 327680,
|
||||
"maximum_size": 16777216,
|
||||
"use_1200bps_touch": true,
|
||||
"wait_for_upload_port": true,
|
||||
"require_upload_port": true,
|
||||
"speed": 921600
|
||||
},
|
||||
"url": "https://heltec.org/",
|
||||
"vendor": "heltec"
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"memory_type": "qio_opi"
|
||||
},
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=0"
|
||||
],
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"flash_mode": "qio",
|
||||
"hwids": [["0x303A", "0x1001"]],
|
||||
"mcu": "esp32s3",
|
||||
"variant": "station-g3"
|
||||
},
|
||||
"connectivity": ["wifi", "bluetooth", "lora"],
|
||||
"debug": {
|
||||
"default_tool": "esp-builtin",
|
||||
"onboard_tools": ["esp-builtin"],
|
||||
"openocd_target": "esp32s3.cfg"
|
||||
},
|
||||
"frameworks": ["arduino", "espidf"],
|
||||
"name": "BQ Station G3",
|
||||
"upload": {
|
||||
"flash_size": "16MB",
|
||||
"maximum_ram_size": 327680,
|
||||
"maximum_size": 16777216,
|
||||
"use_1200bps_touch": true,
|
||||
"wait_for_upload_port": true,
|
||||
"require_upload_port": true,
|
||||
"speed": 921600
|
||||
},
|
||||
"url": "",
|
||||
"vendor": "BQ Consulting"
|
||||
}
|
||||
Vendored
+12
@@ -1,3 +1,15 @@
|
||||
meshtasticd (2.7.24.0) unstable; urgency=medium
|
||||
|
||||
* Version 2.7.24
|
||||
|
||||
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Fri, 08 May 2026 10:44:12 +0000
|
||||
|
||||
meshtasticd (2.7.23.0) unstable; urgency=medium
|
||||
|
||||
* Version 2.7.23
|
||||
|
||||
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Tue, 14 Apr 2026 12:29:48 +0000
|
||||
|
||||
meshtasticd (2.7.22.0) unstable; urgency=medium
|
||||
|
||||
* Version 2.7.22
|
||||
|
||||
Vendored
+1
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/bash
|
||||
set -e
|
||||
export DEBEMAIL="jbennett@incomsystems.biz"
|
||||
export PLATFORMIO_LIBDEPS_DIR=pio/libdeps
|
||||
export PLATFORMIO_PACKAGES_DIR=pio/packages
|
||||
|
||||
@@ -82,5 +82,18 @@ if esp32_kind == "esp32":
|
||||
]
|
||||
)
|
||||
else:
|
||||
# For newer ESP32 targets, using newlib nano works better.
|
||||
env.Append(LINKFLAGS=["--specs=nano.specs", "-u", "_printf_float"])
|
||||
# For newer ESP32 targets, using newlib nano works better. Skip on
|
||||
# variants that explicitly opt out — the IDF 5.1 framework override the
|
||||
# HaLow variant uses already includes nano.specs, so re-adding it triggers
|
||||
# a duplicate spec definition error at link time.
|
||||
cppdefines = env.get("CPPDEFINES", [])
|
||||
if not any(
|
||||
(isinstance(d, str) and d == "MESHTASTIC_SKIP_NANO_SPECS")
|
||||
or (
|
||||
isinstance(d, (list, tuple))
|
||||
and len(d) > 0
|
||||
and d[0] == "MESHTASTIC_SKIP_NANO_SPECS"
|
||||
)
|
||||
for d in cppdefines
|
||||
):
|
||||
env.Append(LINKFLAGS=["--specs=nano.specs", "-u", "_printf_float"])
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
PlatformIO doesn't natively link .o files vendored inside a library directory.
|
||||
The Morse Micro SDK ships the chip firmware (mm6108.mbin.o) and per-region BCF
|
||||
(bcf_mf08651_us.mbin.o) as pre-built object files containing data sections.
|
||||
This script appends them to LINKFLAGS so they land in the final ELF.
|
||||
|
||||
US region only for now — when we add EU/JP/KR variants, gate the BCF here on a
|
||||
build flag and pick the matching .o file.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
Import("env", "projenv")
|
||||
|
||||
LIB_DIR = os.path.join(env.subst("$PROJECT_DIR"), "lib", "MorseWlan")
|
||||
|
||||
mbin_objects = [
|
||||
os.path.join(LIB_DIR, "src", "mm6108.mbin.o"),
|
||||
os.path.join(LIB_DIR, "src", "bcf_mf08651_us.mbin.o"),
|
||||
]
|
||||
|
||||
# Only add objects that actually exist; missing ones surface as link errors,
|
||||
# not silent corruption.
|
||||
for obj in mbin_objects:
|
||||
if not os.path.isfile(obj):
|
||||
print("warning: mm-iot-esp32 blob missing: %s" % obj)
|
||||
|
||||
env.Append(LINKFLAGS=mbin_objects)
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2023 Morse Micro
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @defgroup MBIN Morse BINary Loader API
|
||||
*
|
||||
* This file defines the structure of the @c MBIN file.
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifndef PACKED
|
||||
/** Macro for the compiler packed attribute */
|
||||
#define PACKED __attribute__((packed))
|
||||
#endif
|
||||
|
||||
/** Enumeration of TLV field types */
|
||||
enum mbin_tlv_types {
|
||||
FIELD_TYPE_FW_TLV_BCF_ADDR = 0x0001,
|
||||
FIELD_TYPE_MAGIC = 0x8000,
|
||||
FIELD_TYPE_FW_SEGMENT = 0x8001,
|
||||
FIELD_TYPE_FW_SEGMENT_DEFLATED = 0x8002,
|
||||
FIELD_TYPE_BCF_BOARD_CONFIG = 0x8100,
|
||||
FIELD_TYPE_BCF_REGDOM = 0x8101,
|
||||
FIELD_TYPE_BCF_BOARD_DESC = 0x8102,
|
||||
FIELD_TYPE_BCF_BUILD_VER = 0x8103,
|
||||
FIELD_TYPE_SW_SEGMENT = 0x8201,
|
||||
FIELD_TYPE_SW_SEGMENT_DEFLATED = 0x8202,
|
||||
FIELD_TYPE_EOF = 0x8f00,
|
||||
FIELD_TYPE_EOF_WITH_SIGNATURE = 0x8f01,
|
||||
};
|
||||
|
||||
/** TLV header data structure. */
|
||||
struct PACKED mbin_tlv_hdr {
|
||||
/** Type (see mbin_tlv_types). */
|
||||
uint16_t type;
|
||||
/** Length of payload (excludes header). */
|
||||
uint16_t len;
|
||||
};
|
||||
|
||||
/** Data header in a FIELD_TYPE_XX_SEGMENT field. */
|
||||
struct PACKED mbin_segment_hdr {
|
||||
/** Destination base address at which the data should be loaded. */
|
||||
uint32_t base_address;
|
||||
};
|
||||
|
||||
/** Data header in a FIELD_TYPE_XX_SEGMENT_DEFLATED field. */
|
||||
struct PACKED mbin_deflated_segment_hdr {
|
||||
/** Destination base address at which the data should be loaded. */
|
||||
uint32_t base_address;
|
||||
/** Size of deflated data, infer size of compressed data from TLV length */
|
||||
uint16_t chunk_size;
|
||||
/** ZLib header */
|
||||
uint8_t zlib_header[2];
|
||||
};
|
||||
|
||||
/** Data header in a @c FIELD_TYPE_BCF_REGDOM field. */
|
||||
struct PACKED mbin_regdom_hdr {
|
||||
/** Country code that this @c regdom applies to. */
|
||||
uint8_t country_code[2];
|
||||
/** Reserved */
|
||||
uint16_t reserved;
|
||||
};
|
||||
|
||||
/** Expected value of the magic field for a SW image @c MMSW. */
|
||||
#define MBIN_SW_MAGIC_NUMBER (0x57534d4d)
|
||||
/** Expected value of the magic field for a firmware image @c MMFW. */
|
||||
#define MBIN_FW_MAGIC_NUMBER (0x57464d4d)
|
||||
/** Expected value of the magic field for a BCF @c MMBC. */
|
||||
#define MBIN_BCF_MAGIC_NUMBER (0x43424d4d)
|
||||
|
||||
/** @} */
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright 2024 Morse Micro
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file should be included from the application mbedtls_config.h file to ensure that
|
||||
* the mbedTLS features necessary for morselib functionality are enabled.
|
||||
*
|
||||
* It is recommended to include this file at the _end_ of the application mbedtls_config.h file
|
||||
* to avoid redefinition of macros.
|
||||
*/
|
||||
|
||||
/* Cipher modes */
|
||||
#ifndef MBEDTLS_CIPHER_MODE_CBC
|
||||
#define MBEDTLS_CIPHER_MODE_CBC
|
||||
#endif
|
||||
|
||||
#ifndef MBEDTLS_CIPHER_MODE_CTR
|
||||
#define MBEDTLS_CIPHER_MODE_CTR
|
||||
#endif
|
||||
|
||||
/* EC curves */
|
||||
#ifndef MBEDTLS_ECP_DP_SECP256R1_ENABLED
|
||||
#define MBEDTLS_ECP_DP_SECP256R1_ENABLED
|
||||
#endif
|
||||
|
||||
#ifndef MBEDTLS_ECP_DP_SECP384R1_ENABLED
|
||||
#define MBEDTLS_ECP_DP_SECP384R1_ENABLED
|
||||
#endif
|
||||
|
||||
#ifndef MBEDTLS_ECP_DP_SECP521R1_ENABLED
|
||||
#define MBEDTLS_ECP_DP_SECP521R1_ENABLED
|
||||
#endif
|
||||
|
||||
/* Features */
|
||||
#ifndef MBEDTLS_AES_C
|
||||
#define MBEDTLS_AES_C
|
||||
#endif
|
||||
|
||||
#ifndef MBEDTLS_ASN1_PARSE_C
|
||||
#define MBEDTLS_ASN1_PARSE_C
|
||||
#endif
|
||||
|
||||
#ifndef MBEDTLS_ASN1_WRITE_C
|
||||
#define MBEDTLS_ASN1_WRITE_C
|
||||
#endif
|
||||
|
||||
#ifndef MBEDTLS_BIGNUM_C
|
||||
#define MBEDTLS_BIGNUM_C
|
||||
#endif
|
||||
|
||||
#ifndef MBEDTLS_CIPHER_C
|
||||
#define MBEDTLS_CIPHER_C
|
||||
#endif
|
||||
|
||||
#ifndef MBEDTLS_CMAC_C
|
||||
#define MBEDTLS_CMAC_C
|
||||
#endif
|
||||
|
||||
#ifndef MBEDTLS_CTR_DRBG_C
|
||||
#define MBEDTLS_CTR_DRBG_C
|
||||
#endif
|
||||
|
||||
#ifndef MBEDTLS_ECDH_C
|
||||
#define MBEDTLS_ECDH_C
|
||||
#endif
|
||||
|
||||
#ifndef MBEDTLS_ECP_C
|
||||
#define MBEDTLS_ECP_C
|
||||
#endif
|
||||
|
||||
#ifndef MBEDTLS_ENTROPY_C
|
||||
#define MBEDTLS_ENTROPY_C
|
||||
#endif
|
||||
|
||||
#ifndef MBEDTLS_MD_C
|
||||
#define MBEDTLS_MD_C
|
||||
#endif
|
||||
|
||||
#ifndef MBEDTLS_NIST_KW_C
|
||||
#define MBEDTLS_NIST_KW_C
|
||||
#endif
|
||||
|
||||
#ifndef MBEDTLS_OID_C
|
||||
#define MBEDTLS_OID_C
|
||||
#endif
|
||||
|
||||
#ifndef MBEDTLS_PK_C
|
||||
#define MBEDTLS_PK_C
|
||||
#endif
|
||||
|
||||
#ifndef MBEDTLS_PK_PARSE_C
|
||||
#define MBEDTLS_PK_PARSE_C
|
||||
#endif
|
||||
|
||||
#ifndef MBEDTLS_PK_WRITE_C
|
||||
#define MBEDTLS_PK_WRITE_C
|
||||
#endif
|
||||
|
||||
#ifndef MBEDTLS_PKCS5_C
|
||||
#define MBEDTLS_PKCS5_C
|
||||
#endif
|
||||
|
||||
#ifndef MBEDTLS_SHA1_C
|
||||
#define MBEDTLS_SHA1_C
|
||||
#endif
|
||||
|
||||
#ifndef MBEDTLS_SHA224_C
|
||||
#define MBEDTLS_SHA224_C
|
||||
#endif
|
||||
|
||||
#ifndef MBEDTLS_SHA256_C
|
||||
#define MBEDTLS_SHA256_C
|
||||
#endif
|
||||
|
||||
#ifndef MBEDTLS_SHA384_C
|
||||
#define MBEDTLS_SHA384_C
|
||||
#endif
|
||||
|
||||
#ifndef MBEDTLS_SHA512_C
|
||||
#define MBEDTLS_SHA512_C
|
||||
#endif
|
||||
@@ -0,0 +1,331 @@
|
||||
/*
|
||||
* Copyright 2021-2023 Morse Micro
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @defgroup MMHAL Morse Micro Hardware Abstraction Layer (mmhal) API
|
||||
*
|
||||
* This API provides abstraction from the underlying hardware/BSP.
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "mmhal_flash.h"
|
||||
#include "mmhal_wlan.h"
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Initialization before RTOS scheduler starts. */
|
||||
void mmhal_early_init(void);
|
||||
|
||||
/** Initialization after RTOS scheduler started. */
|
||||
void mmhal_init(void);
|
||||
|
||||
/** Enumeration of ISR states (i.e., whether in ISR or not). */
|
||||
enum mmhal_isr_state {
|
||||
MMHAL_NOT_IN_ISR, /**< The function was not executed from ISR context. */
|
||||
MMHAL_IN_ISR, /**< The function was executed from ISR context. */
|
||||
MMHAL_ISR_STATE_UNKNOWN, /**< The HAL does not support checking ISR state. */
|
||||
};
|
||||
|
||||
/**
|
||||
* Enumeration for different LED's on the board.
|
||||
*
|
||||
* @note Some of these LED's may not be available on all boards and some of these values
|
||||
* may refer to the same LED.
|
||||
*/
|
||||
enum mmhal_led_id { LED_RED, LED_GREEN, LED_BLUE, LED_WHITE };
|
||||
|
||||
/** Enumeration of MCU sleep state. */
|
||||
enum mmhal_sleep_state {
|
||||
MMHAL_SLEEP_DISABLED, /**< Disable MCU sleep. */
|
||||
MMHAL_SLEEP_SHALLOW, /**< MCU to enter shallow sleep. */
|
||||
MMHAL_SLEEP_DEEP, /**< MCU can enter deep sleep. */
|
||||
};
|
||||
|
||||
/** A value of 0 turns OFF an LED */
|
||||
#define LED_OFF 0
|
||||
|
||||
/**
|
||||
* A value of 255 turns an LED ON fully.
|
||||
*
|
||||
* Some boards support varying an LED's brightness. For these boards a value between 1 and 255
|
||||
* will result in proportionately varying levels of brightness. LED's that do not have a
|
||||
* brightness control feature will just turn ON fully for any non zero value.
|
||||
*/
|
||||
#define LED_ON 255
|
||||
|
||||
/**
|
||||
* Get the current ISR state (i.e., whether in ISR or not).
|
||||
*
|
||||
* @returns the current ISR state, or @c MMHAL_ISR_STATE_UNKNOWN if the HAL does not support
|
||||
* checking ISR state.
|
||||
*/
|
||||
enum mmhal_isr_state mmhal_get_isr_state(void);
|
||||
|
||||
/**
|
||||
* Write to the debug log.
|
||||
*
|
||||
* It is assumed the caller will have mechanisms in place to prevent concurrent access.
|
||||
*
|
||||
* @param data Buffer containing data to write.
|
||||
* @param len Length of data in buffer.
|
||||
*/
|
||||
void mmhal_log_write(const uint8_t *data, size_t len);
|
||||
|
||||
/**
|
||||
* Flush the debug log before returning.
|
||||
*
|
||||
* @warning Implementations of this function must support being invoked with interrupts disabled.
|
||||
*/
|
||||
void mmhal_log_flush(void);
|
||||
|
||||
/**
|
||||
* Generate a random 32 bit integer within the given range.
|
||||
*
|
||||
* @param min Minimum value (inclusive).
|
||||
* @param max Maximum value (inclusive).
|
||||
*
|
||||
* @returns a randomly generated integer (min <= i <= max).
|
||||
*/
|
||||
uint32_t mmhal_random_u32(uint32_t min, uint32_t max);
|
||||
|
||||
/** Reset the microcontroller. */
|
||||
void mmhal_reset(void);
|
||||
|
||||
/**
|
||||
* Set the specified LED to the requested level. Do nothing if the requested LED does not exist.
|
||||
*
|
||||
* @param led The LED to set, if the platform supports it. See @ref mmhal_led_id
|
||||
* @param level The level to set it to. 0 means OFF and non-zero means ON. If the platform supports
|
||||
* brightness levels then 255 is full. Defines @ref LED_ON and @ref LED_OFF are
|
||||
* provided for ease of use.
|
||||
*/
|
||||
void mmhal_set_led(uint8_t led, uint8_t level);
|
||||
|
||||
/**
|
||||
* Set the error LED to the requested state.
|
||||
*
|
||||
* @note This function is called by the bootloader and so needs to do all the initialization
|
||||
* required to set the LED's as the bootloader does not use the regular BSP initialization
|
||||
* located in main.c for configuring @c GPIO's and setting clock gates as required.
|
||||
*
|
||||
* @param state Set to true if the LED needs to be turned on,
|
||||
* or false if the led needs to be turned off.
|
||||
*/
|
||||
void mmhal_set_error_led(bool state);
|
||||
|
||||
/**
|
||||
* Enumeration for buttons on the board.
|
||||
*
|
||||
* @note The support for each button is platform dependent.
|
||||
*/
|
||||
enum mmhal_button_id { BUTTON_ID_USER0 };
|
||||
|
||||
/**
|
||||
* Enumeration for button states
|
||||
*/
|
||||
enum mmhal_button_state { BUTTON_RELEASED, BUTTON_PRESSED };
|
||||
|
||||
/** Button state callback function prototype. */
|
||||
typedef void (*mmhal_button_state_cb_t)(enum mmhal_button_id button_id, enum mmhal_button_state button_state);
|
||||
|
||||
/**
|
||||
* Registers a callback handler for button state changes.
|
||||
*
|
||||
* @note The callback will be executed in an Interrupt Service Routine context
|
||||
*
|
||||
* @param button_id The button whose state should be reported to the callback
|
||||
* @param button_state_cb The function to call on button state change, or NULL to disable.
|
||||
* @returns True if the callback is registered successfully, False if not supported
|
||||
*/
|
||||
bool mmhal_set_button_callback(enum mmhal_button_id button_id, mmhal_button_state_cb_t button_state_cb);
|
||||
|
||||
/**
|
||||
* Returns the registered callback handler for button state changes.
|
||||
*
|
||||
* @param button_id The button whose callback should be returned
|
||||
* @returns The registered callback or NULL if no callback registered
|
||||
*/
|
||||
mmhal_button_state_cb_t mmhal_get_button_callback(enum mmhal_button_id button_id);
|
||||
|
||||
/**
|
||||
* Reads the state of the specified button.
|
||||
*
|
||||
* @param button_id The button state to read
|
||||
* @returns The current button state, or BUTTON_RELEASED if not supported
|
||||
*/
|
||||
enum mmhal_button_state mmhal_get_button(enum mmhal_button_id button_id);
|
||||
|
||||
/**
|
||||
* Reads information that can be used to identify the hardware platform, such as
|
||||
* hardware ID and version number, in the form of a user readable string.
|
||||
*
|
||||
* This function attempts to detect the correct hardware and version.
|
||||
* The actual means of detecting the correct hardware and version will vary from
|
||||
* implementation to implementation and may use means such as identification
|
||||
* information stored in EEPROM or devices detected on GPIO, SPI or I2C interfaces.
|
||||
* Returns false if the hardware could not be identified correctly.
|
||||
*
|
||||
* @param version_buffer The pre-allocated buffer to return the hardware ID and version in.
|
||||
* @param version_buffer_length The length of the pre-allocated buffer.
|
||||
* @returns True if the hardware was correctly identified and returned.
|
||||
*/
|
||||
bool mmhal_get_hardware_version(char *version_buffer, size_t version_buffer_length);
|
||||
|
||||
/**
|
||||
* Macro to simplify debug pin mask/value definition.
|
||||
*
|
||||
* @param _pin_num The pin number to set in the mask. Must be 0-31 (inclusive).
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* mmhal_set_debug_pins(MMHAL_DEBUG_PIN(0), MMHAL_DEBUG_PIN(0));
|
||||
*/
|
||||
#define MMHAL_DEBUG_PIN(_pin_num) (1ul << (_pin_num))
|
||||
|
||||
/** Bit mask with all debug pins selected. */
|
||||
#define MMHAL_ALL_DEBUG_PINS (UINT32_MAX)
|
||||
|
||||
/**
|
||||
* Set the value one or more debug pins.
|
||||
*
|
||||
* Each platform can define up to 32 GPIOs for application use. If a GPIO is not supported
|
||||
* by a platform then attempts to set it will be silently ignored. These GPIOs are intended
|
||||
* for debug/test purposes.
|
||||
*
|
||||
* @param mask Mask of GPIOs to modify. Each bit in this mask that is set will result in
|
||||
* the corresponding GPIO being being set to the corresponding value given in
|
||||
* @p values.
|
||||
* @param values Bit field, where each bit corresponds to a GPIO, specifying the direction
|
||||
* to drive each GPIO with 1 meaning drive high and 0 meaning drive low.
|
||||
* Only GPIOs with the corresponding bit set in @p mask will be modified.
|
||||
*
|
||||
* @sa MM_DEBUG_PIN_MASK
|
||||
*/
|
||||
void mmhal_set_debug_pins(uint32_t mask, uint32_t values);
|
||||
|
||||
/**
|
||||
* Returns the time of day as set in the RTC.
|
||||
* Time is in UTC.
|
||||
*
|
||||
* @return Epoch time (seconds since 1 Jan 1970) or 0 on failure.
|
||||
*/
|
||||
time_t mmhal_get_time();
|
||||
|
||||
/**
|
||||
* Sets the RTC to the specified time in UTC.
|
||||
*
|
||||
* @note While Unix epoch time supports years from 1970, most Real Time Clock
|
||||
* chips support years from 2000 only as they store the year as years
|
||||
* from 2000. So do not attempt to set any years below 2000 as this could cause
|
||||
* the year to wrap around to an unreasonably high value. Definitely do not do:
|
||||
* @code
|
||||
* mmhal_set_time(0);
|
||||
* @endcode
|
||||
*
|
||||
* @param epoch Time in Unix epoch time (seconds since 1 Jan 1970).
|
||||
*/
|
||||
void mmhal_set_time(time_t epoch);
|
||||
|
||||
/**
|
||||
* Function to prepare MCU to enter sleep.
|
||||
* This will halt the timer that generates the RTOS tick.
|
||||
*
|
||||
* @param expected_idle_time_ms Expected time to sleep in milliseconds.
|
||||
*
|
||||
* @returns the type of sleep permitted by the current system state.
|
||||
*/
|
||||
enum mmhal_sleep_state mmhal_sleep_prepare(uint32_t expected_idle_time_ms);
|
||||
|
||||
/**
|
||||
* Function to enter MCU sleep.
|
||||
*
|
||||
* @param sleep_state Sleep state to enter into.
|
||||
* @param expected_idle_time_ms Expected time to sleep in milliseconds.
|
||||
*
|
||||
* @returns Elapsed sleep time in milliseconds.
|
||||
*/
|
||||
uint32_t mmhal_sleep(enum mmhal_sleep_state sleep_state, uint32_t expected_idle_time_ms);
|
||||
|
||||
/**
|
||||
* Function to abort the MCU sleep state.
|
||||
*
|
||||
* @note This must only be invoked after @ref mmhal_sleep_prepare()
|
||||
* and before @ref mmhal_sleep().
|
||||
*
|
||||
* @param sleep_state Sleep state to abort.
|
||||
*/
|
||||
void mmhal_sleep_abort(enum mmhal_sleep_state sleep_state);
|
||||
|
||||
/**
|
||||
* Function to cleanup on exit from the MCU sleep state.
|
||||
*/
|
||||
void mmhal_sleep_cleanup(void);
|
||||
|
||||
/** Enumeration of veto_id ranges for use with @ref mmhal_set_deep_sleep_veto() and
|
||||
* @ref mmhal_clear_deep_sleep_veto(). */
|
||||
enum mmhal_veto_id {
|
||||
/** Start of deep sleep veto ID range that is available for application use. */
|
||||
MMHAL_VETO_ID_APP_MIN = 0,
|
||||
/** End of deep sleep veto ID range that is available for application use. */
|
||||
MMHAL_VETO_ID_APP_MAX = 7,
|
||||
/** Start of deep sleep veto ID range that is available for HAL use. */
|
||||
MMHAL_VETO_ID_HAL_MIN = 8,
|
||||
/** End of deep sleep veto ID range that is available for HAL use. */
|
||||
MMHAL_VETO_ID_HAL_MAX = 15,
|
||||
/** Start of deep sleep veto ID range that is allocated for morselib use. Note that this
|
||||
* must not be changed as it is built into morselib. */
|
||||
MMHAL_VETO_ID_MORSELIB_MIN = 16,
|
||||
/** End of deep sleep veto ID range that is allocated for morselib use. Note that this must not
|
||||
* be changed as it is built into morselib. */
|
||||
MMHAL_VETO_ID_MORSELIB_MAX = 19,
|
||||
/** Deep sleep veto ID for data-link subsystem. */
|
||||
MMHAL_VETO_ID_DATALINK = 20,
|
||||
/** Deep sleep veto ID allocated to @ref MMCONFIG. */
|
||||
MMHAL_VETO_ID_MMCONFIG = 21,
|
||||
/** Start of deep sleep veto ID range reserved for future use. */
|
||||
MMHAL_VETO_ID_RESERVED_MIN = 22,
|
||||
/** End of deep sleep veto ID range reserved for future use. */
|
||||
MMHAL_VETO_ID_RESERVED_MAX = 31,
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets a deep sleep veto that will prevent the device from entering deep sleep. The device
|
||||
* will not enter deep sleep until there are no vetoes remaining. This veto can be cleared
|
||||
* by a call to @ref mmhal_clear_deep_sleep_veto() with the same veto_id.
|
||||
*
|
||||
* Up to 32 vetoes are supported (ID numbers 0-31). Each veto should be used exclusively by
|
||||
* a given aspect of the system (e.g., to prevent deep sleep when a DMA transfer is in progress,
|
||||
* or to prevent deep sleep when there is log data buffered for transmit, etc.).
|
||||
*
|
||||
* @param veto_id The veto identifier. Valid values are 0-31, and these are split up into ranges
|
||||
* for use by different subsystems -- see @ref mmhal_veto_id.
|
||||
*/
|
||||
void mmhal_set_deep_sleep_veto(uint8_t veto_id);
|
||||
|
||||
/**
|
||||
* Clears a deep sleep veto that was preventing the device from entering deep sleep (see
|
||||
* @ref mmhal_set_deep_sleep_veto()). If the given veto was not already set then this has
|
||||
* no effect.
|
||||
*
|
||||
* @param veto_id The veto identifier. Valid values are 0-31, and these are split up into ranges
|
||||
* for use by different subsystems -- see @ref mmhal_veto_id.
|
||||
*/
|
||||
void mmhal_clear_deep_sleep_veto(uint8_t veto_id);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
/** @} */
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Copyright 2021-2023 Morse Micro
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @ingroup MMHAL Morse Micro Flash Hardware Abstraction Layer (mmhal_flash) API
|
||||
*
|
||||
* This API provides abstraction from the underlying flash hardware/.
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* This is the value erased flash bytes are set to. This shall be @c 0xFF as this is the
|
||||
* value that hardware flash erases to.
|
||||
*/
|
||||
#define MMHAL_FLASH_ERASE_VALUE 0xFF
|
||||
|
||||
/** LittleFS configuration structure. Include @c lfs.h for definition. */
|
||||
struct lfs_config;
|
||||
|
||||
/**
|
||||
* Flash partition configuration structure
|
||||
*
|
||||
* This should be initialized using @c MMHAL_FLASH_PARTITION_CONFIG_DEFAULT.
|
||||
* For example:
|
||||
*
|
||||
* @code{.c}
|
||||
* struct mmhal_flash_partition_config partition = MMHAL_FLASH_PARTITION_CONFIG_DEFAULT;
|
||||
* @endcode
|
||||
*/
|
||||
struct mmhal_flash_partition_config {
|
||||
/** The start address of the partition, this may be a physical address
|
||||
* or a relative address depending on implementation
|
||||
*/
|
||||
uint32_t partition_start;
|
||||
|
||||
/** The size of the partition */
|
||||
uint32_t partition_size;
|
||||
|
||||
/**
|
||||
* If true, then the partition is not memory mapped and cannot be directly accessed
|
||||
* at the physical @c partition_start address
|
||||
*/
|
||||
bool not_memory_mapped;
|
||||
};
|
||||
|
||||
/** Initial values for @ref mmhal_flash_partition_config. */
|
||||
#define MMHAL_FLASH_PARTITION_CONFIG_DEFAULT \
|
||||
{ \
|
||||
0, 0, false \
|
||||
}
|
||||
|
||||
/**
|
||||
* Get MMCONFIG flash partition configuration.
|
||||
*
|
||||
* MMCONFIG initialization is done by @c mmconfig_init() in @c mmconfig.c.
|
||||
* which in turn calls this function to fetch the partition configuration for config store
|
||||
* from the HAL layer. If config store is not supported by the platform then we just
|
||||
* return NULL. This function returns a static pointer to
|
||||
* @c struct @c mmhal_flash_partition_config.
|
||||
*
|
||||
* @return A static pointer to the partition config for MMCONFIG, or NULL if not supported.
|
||||
*/
|
||||
const struct mmhal_flash_partition_config *mmhal_get_mmconfig_partition(void);
|
||||
|
||||
/**
|
||||
* Erases a block of Flash pointed to by the block_address.
|
||||
*
|
||||
* The block address may be anywhere within the block to erase. The entire block gets erased.
|
||||
* Once erased all bytes in the block shall be @c MMHAL_FLASH_ERASE_VALUE (@c 0xFF).
|
||||
*
|
||||
* @param block_address The address of the block of Flash to erase.
|
||||
* @return 0 on success, negative number on failure
|
||||
*/
|
||||
int mmhal_flash_erase(uint32_t block_address);
|
||||
|
||||
/**
|
||||
* Returns the size of the Flash block at the specified address.
|
||||
*
|
||||
* @param block_address The address of the Flash block.
|
||||
* @return The size of the Flash block in bytes.
|
||||
* Returns 0 if an invalid address is specified.
|
||||
*/
|
||||
uint32_t mmhal_flash_getblocksize(uint32_t block_address);
|
||||
|
||||
/**
|
||||
* Read a block of data from the specified Flash address into the buffer.
|
||||
*
|
||||
* @param read_address The address in Flash to read from.
|
||||
* @param buf The buffer to read into.
|
||||
* @param size The number of bytes to read.
|
||||
* @return 0 on success, or a negative number on failure.
|
||||
*/
|
||||
int mmhal_flash_read(uint32_t read_address, uint8_t *buf, size_t size);
|
||||
|
||||
/**
|
||||
* Write a block of data to the specified Flash address.
|
||||
*
|
||||
* There is no alignment or minimum size requirement. This function will
|
||||
* take care of aligning the data and merging with existing Flash contents.
|
||||
* The Flash block is not erased, it is up to the application to determine
|
||||
* if the block needs to be erased before programming.
|
||||
*
|
||||
* @param write_address The address in Flash to write to.
|
||||
* @param data A pointer to the block of data to write.
|
||||
* @param size The number of bytes to write.
|
||||
* @return 0 on success, or a negative number on failure.
|
||||
*/
|
||||
int mmhal_flash_write(uint32_t write_address, const uint8_t *data, size_t size);
|
||||
|
||||
/**
|
||||
* Get LittleFS configuration.
|
||||
*
|
||||
* LittleFS initialization is done by @c littlefs_init() in @c mmosal_shim_fileio.c.
|
||||
* which in turn calls this function to fetch the hardware configuration for LittleFS
|
||||
* from the HAL layer. The LittleFS configuration will vary from platform to platform.
|
||||
* If LittleFS is not supported by the platform then we just return NULL. This function
|
||||
* returns a static pointer to @c struct @c lfs_config which is defined in @c lfs.h.
|
||||
*
|
||||
* See @c mmhal_littlefs.c for the full HAL layer implementation for your platform.
|
||||
* See @c mmosal_shim_fileio.c for the @c libc shims for LittleFS.
|
||||
* See @c README.md in the @c src/littlefs folder for detailed information on LittleFS.
|
||||
*
|
||||
* @return A static pointer to the LittleFS config structure, or NULL if not supported.
|
||||
*/
|
||||
const struct lfs_config *mmhal_get_littlefs_config(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
/** @} */
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2024 Morse Micro
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @ingroup MMHAL
|
||||
* @defgroup MMHAL_UART Morse Micro Abstraction Layer API for UART
|
||||
*
|
||||
* This provides an abstraction layer for a UART. This is used by MM-IoT-SDK example
|
||||
* applications.
|
||||
*
|
||||
* This is a very simple API and leaves UART configuration to the HAL.
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "mmhal.h"
|
||||
#include "mmosal.h"
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Function type for UART RX callback.
|
||||
*
|
||||
* @note The UART HAL must not invoke this function from interrupt context. However, the
|
||||
* implementation should not block for long periods of time or received data may be lost.
|
||||
*
|
||||
* @param data The received data.
|
||||
* @param length Length of the received data.
|
||||
* @param arg Opaque argument (as passed in to @ref mmhal_uart_init()).
|
||||
*/
|
||||
typedef void (*mmhal_uart_rx_cb_t)(const uint8_t *data, size_t length, void *arg);
|
||||
|
||||
/**
|
||||
* Initialize the UART HAL and perform any setup necessary.
|
||||
*
|
||||
* @param rx_cb Optional callback to be invoked on receive (may be NULL).
|
||||
* This callback may be invoked from interrupt context so should return
|
||||
* quickly.
|
||||
* @param rx_cb_arg Optional opaque argument to be passed to the RX callback. May be NULL.
|
||||
*/
|
||||
void mmhal_uart_init(mmhal_uart_rx_cb_t rx_cb, void *rx_cb_arg);
|
||||
|
||||
/**
|
||||
* Deinitialize the UART HAL, and disable the UART.
|
||||
*/
|
||||
void mmhal_uart_deinit(void);
|
||||
|
||||
/**
|
||||
* Transmit data on the UART. This will block until all data is buffered for transmit (but may
|
||||
* return before transmission has completed).
|
||||
*
|
||||
* @param data Data to transmit.
|
||||
* @param length Length of @p data.
|
||||
*/
|
||||
void mmhal_uart_tx(const uint8_t *data, size_t length);
|
||||
|
||||
/** Enumeration of deep sleep modes for the UART HAL. */
|
||||
enum mmhal_uart_deep_sleep_mode {
|
||||
/** Deep sleep mode is disabled. */
|
||||
MMHAL_UART_DEEP_SLEEP_DISABLED,
|
||||
/** Enable deep sleep until activity occurs on data-link transport. */
|
||||
MMHAL_UART_DEEP_SLEEP_ONE_SHOT,
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the deep sleep mode for the UART. See @ref mmhal_uart_deep_sleep_mode for possible deep
|
||||
* sleep modes. Note that a given platform may not support all modes.
|
||||
*
|
||||
* @param mode The deep sleep mode to set.
|
||||
*
|
||||
* @returns true if the mode was set successfully; false on failure (e.g., unsupported mode).
|
||||
*/
|
||||
bool mmhal_uart_set_deep_sleep_mode(enum mmhal_uart_deep_sleep_mode mode);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
/** @} */
|
||||
@@ -0,0 +1,640 @@
|
||||
/*
|
||||
* Copyright 2023 Morse Micro
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @ingroup MMHAL
|
||||
* @defgroup MMHAL_WLAN WLAN HAL
|
||||
*
|
||||
* API for communicating with the WLAN transceiver.
|
||||
*
|
||||
* There are different interfaces supported for communicating with the transceiver:
|
||||
*
|
||||
* * @ref MMHAL_WLAN_SDIO
|
||||
* * @ref MMHAL_WLAN_SPI
|
||||
*
|
||||
* @warning These functions shall not be called directly by the end application they are for use
|
||||
* by Morselib.
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "mmpkt.h"
|
||||
#include "mmwlan.h"
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Function prototype for interrupt handler callbacks.
|
||||
*/
|
||||
typedef void (*mmhal_irq_handler_t)(void);
|
||||
|
||||
/**
|
||||
* Initialize the WLAN HAL.
|
||||
*
|
||||
* Things to do here may include:
|
||||
* * Enable SPI peripheral
|
||||
* * Configure GPIOs
|
||||
* * Enable power to the Morse Micro transceiver
|
||||
*
|
||||
* @note If enabling power for the Morse Micro transceiver in this function you may need to add a
|
||||
* blocking delay to allow the power rail to stabilize. This is hardware specific so is not
|
||||
* accounted for in the calling function.
|
||||
*/
|
||||
void mmhal_wlan_init(void);
|
||||
|
||||
/**
|
||||
* Deinitialize the WLAN HAL.
|
||||
*
|
||||
* Things to do here may include:
|
||||
* * Disable SPI peripheral
|
||||
* * Disable GPIOs
|
||||
* * Disable power to the Morse Micro transceiver
|
||||
*/
|
||||
void mmhal_wlan_deinit(void);
|
||||
|
||||
/**
|
||||
* Get MAC address override.
|
||||
*
|
||||
* This function allows the HAL to override the MAC address to be used by the device. The
|
||||
* MAC address override should be written to @p mac_addr. If no override is required then
|
||||
* @p mac_addr should be left untouched.
|
||||
*
|
||||
* @param[out] mac_addr Location where the MAC address will be stored. This will be initialized
|
||||
* to zero the first time this function is invoked, and to the previously
|
||||
* configured MAC address on subsequent invocations.
|
||||
*/
|
||||
void mmhal_read_mac_addr(uint8_t *mac_addr);
|
||||
|
||||
/**
|
||||
* Assert the WLAN wake pin.
|
||||
*/
|
||||
void mmhal_wlan_wake_assert(void);
|
||||
|
||||
/**
|
||||
* Deassert the WLAN wake pin.
|
||||
*/
|
||||
void mmhal_wlan_wake_deassert(void);
|
||||
|
||||
/**
|
||||
* Tests whether the busy pin is currently asserted.
|
||||
*
|
||||
* @note This is whether it is logically asserted and does not necessarily
|
||||
* represent the level of the GPIO pin.
|
||||
*
|
||||
* @returns @c true if asserted, else @c false.
|
||||
*/
|
||||
bool mmhal_wlan_busy_is_asserted(void);
|
||||
|
||||
/**
|
||||
* Register a handler for busy interrupts.
|
||||
*
|
||||
* @param handler The handler to register.
|
||||
*/
|
||||
void mmhal_wlan_register_busy_irq_handler(mmhal_irq_handler_t handler);
|
||||
|
||||
/**
|
||||
* Sets whether the busy interrupt is enabled.
|
||||
*
|
||||
* @warning The interrupt handler function must be configured using
|
||||
* mmhal_wlan_register_busy_irq_handler() before enabling the interrupt.
|
||||
*
|
||||
* @param enabled @c true to enable or @c false to disable.
|
||||
*/
|
||||
void mmhal_wlan_set_busy_irq_enabled(bool enabled);
|
||||
|
||||
/**
|
||||
* Read-only buffer data structure.
|
||||
*
|
||||
* The design of this data structure allows the buffer to exist either in statically or
|
||||
* dynamically allocated memory.
|
||||
*
|
||||
* For statically allocated memory, the field @c free_cb may be set to @c NULL and @c free_arg
|
||||
* ignored. For example:
|
||||
*
|
||||
* @code{.c}
|
||||
* const uint8_t some_data[] = { 0x00, 0x01, 0x02 };
|
||||
*
|
||||
* void put_some_data_into_buf(struct mmhal_robuf *robuf)
|
||||
* {
|
||||
* robuf->buf = some_data;
|
||||
* robuf->len = sizeof(some_data);
|
||||
* robuf->free_cb = NULL;
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* For dynamically allocated memory, the field @c free_cb is set to the appropriate function to
|
||||
* free the buffer and @c free_arg is an opaque argument to the free function. This approach might
|
||||
* be used, for example, when reading into a temporary buffer from storage that is not memory
|
||||
* mapped. For example:
|
||||
*
|
||||
* @code{.c}
|
||||
* #define SOME_DATA_MAXLEN (64)
|
||||
*
|
||||
* void put_some_data_into_buf(struct mmhal_robuf *robuf)
|
||||
* {
|
||||
* uint8_t *buf = malloc(SOME_DATA_MAXLEN);
|
||||
* robuf->buf = buf;
|
||||
* if (robuf->buf == NULL)
|
||||
* return;
|
||||
*
|
||||
* // HERE: copy data into buf and set robuf->len as appropriate
|
||||
*
|
||||
* robuf->free_cb = free;
|
||||
* robuf->free_arg = buf;
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
*/
|
||||
struct mmhal_robuf {
|
||||
/** Pointer to the start of the read-only buffer. May be NULL only if @c len is zero. */
|
||||
const uint8_t *buf;
|
||||
/** Length of the buffer contents. */
|
||||
uint32_t len;
|
||||
/**
|
||||
* Optional callback to be invoked by the consumer to release the buffer when it is
|
||||
* no longer required. If not required, set to @c NULL.
|
||||
*
|
||||
* @note The values of @c buf and @c len in this structure may be modified before
|
||||
* @c free_cb() is invoked. However, the value of @c free_arg will be passed
|
||||
* to @c free_cb().
|
||||
*/
|
||||
void (*free_cb)(void *arg);
|
||||
/** Optional argument to @c free_cb. Ignored if @c free_cb is @c NULL. */
|
||||
void *free_arg;
|
||||
};
|
||||
|
||||
/** Minimum length of data to be returned by @ref mmhal_wlan_read_bcf_file() and
|
||||
* @ref mmhal_wlan_read_fw_file(). */
|
||||
#define MMHAL_WLAN_FW_BCF_MIN_READ_LENGTH (4)
|
||||
|
||||
/**
|
||||
* Retrieves the content of the Morse Micro Board Configuration File and places it into the given
|
||||
* buffer.
|
||||
*
|
||||
* @param offset Offset from which to start reading the bcf
|
||||
* @param requested_len Length of data we would like to read. The length of the data returned
|
||||
* by this function may be less than @p requested_len, but must be at
|
||||
* least @ref MMHAL_WLAN_FW_BCF_MIN_READ_LENGTH.
|
||||
* @param robuf Read-only buffer data structure to be filled out by this function.
|
||||
*
|
||||
* @note On error, this function should set @c robuf->buf to @c NULL.
|
||||
* @note The caller must zero @p robuf before invoking the function.
|
||||
* @note The BCF must be in mbin format.
|
||||
*
|
||||
* @warning The caller is responsible for checking @c robuf->free_cb and calling when the buffer is
|
||||
* no longer required. Ignored if @c robuf->free_cb is @c NULL.
|
||||
*/
|
||||
void mmhal_wlan_read_bcf_file(uint32_t offset, uint32_t requested_len, struct mmhal_robuf *robuf);
|
||||
|
||||
/**
|
||||
* Retrieves the content of the Morse Micro Chip Firmware and places it into the given buffer.
|
||||
*
|
||||
* @param offset Offset from which to start reading the bcf
|
||||
* @param requested_len Length of data we would like to read. The length of the data returned
|
||||
* by this function may be less than @p requested_len, but must be at
|
||||
* least @ref MMHAL_WLAN_FW_BCF_MIN_READ_LENGTH.
|
||||
* @param robuf Read-only buffer data structure to be filled out by this function.
|
||||
*
|
||||
* @note On error, this function should set @c robuf->buf to @c NULL.
|
||||
* @note The caller must zero @p robuf before invoking the function.
|
||||
* @note The firmware must be in mbin format.
|
||||
*
|
||||
* @warning The caller is responsible for checking @c robuf->free_cb and calling when the buffer
|
||||
* is no longer required. Ignored if @c robuf->free_cb is @c NULL.
|
||||
*/
|
||||
void mmhal_wlan_read_fw_file(uint32_t offset, uint32_t requested_len, struct mmhal_robuf *robuf);
|
||||
|
||||
/**
|
||||
* @defgroup MMHAL_WLAN_SPI WLAN HAL API for SPI interface
|
||||
*
|
||||
* API for communicating with the WLAN transceiver over an SPI interface.
|
||||
*
|
||||
* @note These functions should only be implemented if using a SPI interface. They are not
|
||||
* required when using the @ref MMHAL_WLAN_SDIO.
|
||||
*
|
||||
* @warning These functions shall not be called directly by the end application they are for use
|
||||
* by Morselib.
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Assert the WLAN SPI chip select pin.
|
||||
*/
|
||||
void mmhal_wlan_spi_cs_assert(void);
|
||||
|
||||
/**
|
||||
* Deassert the WLAN SPI chip select pin.
|
||||
*/
|
||||
void mmhal_wlan_spi_cs_deassert(void);
|
||||
|
||||
/**
|
||||
* Simultaneously read and write on the SPI bus.
|
||||
*
|
||||
* @param data Data to be written.
|
||||
*
|
||||
* @return the value that was read.
|
||||
*/
|
||||
uint8_t mmhal_wlan_spi_rw(uint8_t data);
|
||||
|
||||
/**
|
||||
* Receive multiple octets of data from SPI bus.
|
||||
*
|
||||
* @param buf The buffer to receive into.
|
||||
* @param len The number of octets to receive.
|
||||
*/
|
||||
void mmhal_wlan_spi_read_buf(uint8_t *buf, unsigned len);
|
||||
|
||||
/**
|
||||
* Transmit multiple octets of data to SPI bus.
|
||||
*
|
||||
* @param buf The buffer to transmit from.
|
||||
* @param len The number of octets to transmit.
|
||||
*
|
||||
* @note Blocks until transfer complete.
|
||||
*/
|
||||
void mmhal_wlan_spi_write_buf(const uint8_t *buf, unsigned len);
|
||||
|
||||
/**
|
||||
* Hard reset the chip by asserting and then releasing the reset pin.
|
||||
*
|
||||
* @warning This function must return with the chip in a fully booted state. i.e only return once
|
||||
* the reset_n line has been high for at least the boot time specified in the data sheet.
|
||||
* Failure to do so may lead to undefined behavior.
|
||||
*/
|
||||
void mmhal_wlan_hard_reset(void);
|
||||
|
||||
/**
|
||||
* Invoked by the driver to check whether the external crystal initialization sequence is required.
|
||||
*
|
||||
* Implementation of this function is optional if the external crystal initialization sequence
|
||||
* is not required. If this function is not implemented then the external crystal initialization
|
||||
* sequence will be disabled. Refer to the data sheet for your module to check if this initialization
|
||||
* is required.
|
||||
*
|
||||
* @returns true if the external crystal initialization sequence is required else false.
|
||||
*/
|
||||
bool mmhal_wlan_ext_xtal_init_is_required(void);
|
||||
|
||||
/**
|
||||
* Issue the training sequence required to put the transceiver into SPI mode.
|
||||
*/
|
||||
void mmhal_wlan_send_training_seq(void);
|
||||
|
||||
/**
|
||||
* Register a handler for SPI interrupts.
|
||||
*
|
||||
* @param handler The handler to register.
|
||||
*/
|
||||
void mmhal_wlan_register_spi_irq_handler(mmhal_irq_handler_t handler);
|
||||
|
||||
/**
|
||||
* Sets whether the SPI interrupt is enabled.
|
||||
*
|
||||
* @warning The interrupt handler function must be configured using
|
||||
* @ref mmhal_wlan_register_spi_irq_handler() before enabling the interrupt.
|
||||
*
|
||||
* @param enabled @c true to enable or @c false to disable.
|
||||
*/
|
||||
void mmhal_wlan_set_spi_irq_enabled(bool enabled);
|
||||
|
||||
/**
|
||||
* Tests whether the SPI interrupt pin is currently asserted.
|
||||
*
|
||||
* @note This is whether it is logically asserted and does not necessarily
|
||||
* represent the level of the GPIO pin.
|
||||
*
|
||||
* @returns @c true if asserted, else @c false.
|
||||
*/
|
||||
bool mmhal_wlan_spi_irq_is_asserted(void);
|
||||
|
||||
/**
|
||||
* Clear the SPI IRQ.
|
||||
*
|
||||
* @deprecated Do not invoke this function because it is deprecated and will be removed from the
|
||||
* mmhal API in a future release. This function need not be implemented as a weak
|
||||
* stub is used in morselib.
|
||||
*/
|
||||
void mmhal_wlan_clear_spi_irq(void);
|
||||
|
||||
/** @} */
|
||||
|
||||
/**
|
||||
* @defgroup MMHAL_WLAN_PKT WLAN HAL API for packet memory allocation
|
||||
*
|
||||
* API for allocating and freeing packet memory.
|
||||
*
|
||||
* @warning These functions shall not be called directly by the end application they are for use
|
||||
* by Morselib.
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Flow control callback that can be invoked by the transmit packet memory manager to pause
|
||||
* and resume the data path in response to resource availability.
|
||||
*
|
||||
* @param state Current flow control state.
|
||||
*/
|
||||
typedef void (*mmhal_wlan_pktmem_tx_flow_control_cb_t)(enum mmwlan_tx_flow_control_state state);
|
||||
|
||||
/** Initialization arguments passed to @ref mmhal_wlan_pktmem_init(). */
|
||||
struct mmhal_wlan_pktmem_init_args {
|
||||
/** Flow control callback that can be used by the transmit packet memory manager. */
|
||||
mmhal_wlan_pktmem_tx_flow_control_cb_t tx_flow_control_cb;
|
||||
};
|
||||
|
||||
/**
|
||||
* Invoked by the driver to initialize the packet memory in the HAL.
|
||||
*
|
||||
* @param args Initialization arguments.
|
||||
*/
|
||||
void mmhal_wlan_pktmem_init(struct mmhal_wlan_pktmem_init_args *args);
|
||||
|
||||
/**
|
||||
* Invoked by the driver to deinitialize the packet memory in the HAL.
|
||||
*
|
||||
* This can free reserved memory and check for memory leaks.
|
||||
*/
|
||||
void mmhal_wlan_pktmem_deinit(void);
|
||||
|
||||
/**
|
||||
* Enumeration of packet classes used by @ref mmhal_wlan_alloc_mmpkt_for_tx().
|
||||
* These definitions must match the corresponding values in @c mmdrv_pkt_class.
|
||||
*/
|
||||
enum mmhal_wlan_pkt_class {
|
||||
MMHAL_WLAN_PKT_DATA_TID0, /**< Data TID0 */
|
||||
MMHAL_WLAN_PKT_DATA_TID1, /**< Data TID1 */
|
||||
MMHAL_WLAN_PKT_DATA_TID2, /**< Data TID2 */
|
||||
MMHAL_WLAN_PKT_DATA_TID3, /**< Data TID3 */
|
||||
MMHAL_WLAN_PKT_DATA_TID4, /**< Data TID4 */
|
||||
MMHAL_WLAN_PKT_DATA_TID5, /**< Data TID5 */
|
||||
MMHAL_WLAN_PKT_DATA_TID6, /**< Data TID6 */
|
||||
MMHAL_WLAN_PKT_DATA_TID7, /**< Data TID7 */
|
||||
MMHAL_WLAN_PKT_MANAGEMENT, /**< 802.11 Management and other important frames */
|
||||
MMHAL_WLAN_PKT_COMMAND, /**< Commands from driver to chip */
|
||||
};
|
||||
|
||||
/**
|
||||
* Allocates an mmpkt for transmission.
|
||||
*
|
||||
* When the pool of mmpkt buffers available for TX is exhausted, the HAL should pause the TX
|
||||
* path using the flow control callback that was registered when @ref mmhal_wlan_pktmem_init()
|
||||
* was invoked. Similarly, when the buffers become available again (and assuming the TX path is
|
||||
* not otherwise blocked) the driver should unpause the TX path.
|
||||
*
|
||||
* @param pkt_class The class of packet (to allow for prioritization).
|
||||
* @param space_at_start Amount of space to allocate at start of mmpkt (for prepend).
|
||||
* @param space_at_end Amount of space to allocate at end of mmpkt (for append).
|
||||
* @param metadata_length Amount of space to allocate for metadata (used internally by the
|
||||
* Morse driver).
|
||||
*
|
||||
* @returns a pointer to the allocated packet on success or @c NULL on allocation failure.
|
||||
*/
|
||||
struct mmpkt *mmhal_wlan_alloc_mmpkt_for_tx(uint8_t pkt_class, uint32_t space_at_start, uint32_t space_at_end,
|
||||
uint32_t metadata_length);
|
||||
|
||||
/**
|
||||
* Allocates an mmpkt for reception.
|
||||
*
|
||||
* @param capacity Amount of space to allocate for data.
|
||||
* @param metadata_length Amount of space to allocate for metadata (used internally by the
|
||||
* Morse driver).
|
||||
*
|
||||
* @returns a pointer to the allocated packet on success or @c NULL on allocation failure.
|
||||
*/
|
||||
struct mmpkt *mmhal_wlan_alloc_mmpkt_for_rx(uint32_t capacity, uint32_t metadata_length);
|
||||
|
||||
/** @} */
|
||||
|
||||
/**
|
||||
* @defgroup MMHAL_WLAN_SDIO WLAN HAL API for SDIO interface
|
||||
*
|
||||
* API for communicating with the WLAN transceiver over an SDIO interface
|
||||
*
|
||||
* @warning These functions shall not be called directly by the end application they are for use
|
||||
* by Morselib.
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
/** Enumeration of error codes that may be returned from @c mmhal_wlan_sdio_XXX() functions. */
|
||||
enum mmhal_sdio_error_codes {
|
||||
/** Invalid argument given (e.g., incorrect buffer alignment). */
|
||||
MMHAL_SDIO_INVALID_ARGUMENT = -1,
|
||||
/** Local hardware error (e.g., issue with SDIO controller). */
|
||||
MMHAL_SDIO_HW_ERROR = -2,
|
||||
/** Timeout executing SDIO command. */
|
||||
MMHAL_SDIO_CMD_TIMEOUT = -3,
|
||||
/** CRC error executing SDIO command. */
|
||||
MMHAL_SDIO_CMD_CRC_ERROR = -4,
|
||||
/** Timeout transferring data. */
|
||||
MMHAL_SDIO_DATA_TIMEOUT = -5,
|
||||
/** CRC error transferring data. */
|
||||
MMHAL_SDIO_DATA_CRC_ERROR = -6,
|
||||
/** Underflow filling SDIO controller FIFO. */
|
||||
MMHAL_SDIO_DATA_UNDERFLOW = -7,
|
||||
/** Overflow reading from SDIO controller FIFO. */
|
||||
MMHAL_SDIO_DATA_OVERRUN = -8,
|
||||
/** Another error not covered by the above error codes. */
|
||||
MMHAL_SDIO_OTHER_ERROR = -9,
|
||||
};
|
||||
|
||||
/**
|
||||
* Perform transport specific startup.
|
||||
*
|
||||
* @returns 0 on success, an error code from @ref mmhal_sdio_error_codes on failure.
|
||||
*/
|
||||
int mmhal_wlan_sdio_startup(void);
|
||||
|
||||
/**
|
||||
* Execute an SDIO command without data.
|
||||
*
|
||||
* @param[in] cmd_idx The Command Index.
|
||||
* @param[in] arg Command argument. This corresponds to the 32 bits of the command between
|
||||
* the Command Index field and the CRC7 field.
|
||||
* @param[out] rsp The contents of the command response between the Command Index field
|
||||
* and the CRC7 field. May be @c NULL if the response is not required.
|
||||
* The returned value is undefined if the return code is not zero.
|
||||
*
|
||||
* @returns 0 on success, an error code from @ref mmhal_sdio_error_codes on failure.
|
||||
*/
|
||||
int mmhal_wlan_sdio_cmd(uint8_t cmd_idx, uint32_t arg, uint32_t *rsp);
|
||||
|
||||
/**
|
||||
* Arguments structure for @ref mmhal_wlan_sdio_cmd53_write().
|
||||
*/
|
||||
struct mmhal_wlan_sdio_cmd53_write_args {
|
||||
/** The SDIO argument. This corresponds to the 32 bits of the command between
|
||||
* the Command Index field and the CRC7 field. */
|
||||
uint32_t sdio_arg;
|
||||
/** Pointer to the data buffer. 32 bit word aligned. */
|
||||
const uint8_t *data;
|
||||
/** Transfer length measured in blocks if block_size is non-zero otherwise in bytes.
|
||||
* If transfer_length is measured in bytes, it will be a multiple of 4. */
|
||||
uint16_t transfer_length;
|
||||
/**
|
||||
* If non-zero this indicates that the data should be transferred in block mode with
|
||||
* the given block size. If zero then the data should be transferred in byte mode and
|
||||
* @c transfer_length is guaranteed to not exceed the block size of the function.
|
||||
*/
|
||||
uint16_t block_size;
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute an SDIO CMD53 write.
|
||||
*
|
||||
* @param args The write arguments.
|
||||
*
|
||||
* @returns 0 on success, an error code from @ref mmhal_sdio_error_codes on failure.
|
||||
*/
|
||||
int mmhal_wlan_sdio_cmd53_write(const struct mmhal_wlan_sdio_cmd53_write_args *args);
|
||||
|
||||
/**
|
||||
* Arguments structure for @ref mmhal_wlan_sdio_cmd53_read().
|
||||
*/
|
||||
struct mmhal_wlan_sdio_cmd53_read_args {
|
||||
/** The SDIO argument. This corresponds to the 32 bits of the command between
|
||||
* the Command Index field and the CRC7 field. */
|
||||
uint32_t sdio_arg;
|
||||
/** Pointer to the data buffer to receive the data. 32 bit word aligned. */
|
||||
uint8_t *data;
|
||||
/** Transfer length measured in blocks if block_size is non-zero otherwise in bytes.
|
||||
* If transfer_length is measured in bytes, it will be a multiple of 4. */
|
||||
uint16_t transfer_length;
|
||||
/**
|
||||
* If non-zero this indicates that the data should be transferred in block mode with
|
||||
* the given block size. If zero then the data should be transferred in byte mode and
|
||||
* @c transfer_length is guaranteed to not exceed the block size of the function.
|
||||
*/
|
||||
uint16_t block_size;
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute an SDIO CMD53 read.
|
||||
*
|
||||
* @param args The read arguments.
|
||||
*
|
||||
* @returns 0 on success, an error code from @ref mmhal_sdio_error_codes on failure.
|
||||
*/
|
||||
int mmhal_wlan_sdio_cmd53_read(const struct mmhal_wlan_sdio_cmd53_read_args *args);
|
||||
|
||||
/**
|
||||
* @defgroup MMHAL_WLAN_SDIO_UTILS SDIO Utilities
|
||||
*
|
||||
* Useful macros and inline utilities function for use by SDIO and SPI HALs.
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
/*
|
||||
* SDIO argument definition, per SDIO Specification Version 4.10, Part E1, Section 5.3.
|
||||
*/
|
||||
|
||||
/** SDIO CMD52/CMD53 R/W flag. */
|
||||
enum mmhal_sdio_rw {
|
||||
MMHAL_SDIO_READ = 0, /**< Read operation */
|
||||
MMHAL_SDIO_WRITE = (1ul << 31), /**< Write operation */
|
||||
};
|
||||
|
||||
/** SDIO CMD52/CMD53 function number. */
|
||||
enum mmhal_sdio_function {
|
||||
MMHAL_SDIO_FUNCTION_0 = 0, /** Function 0 */
|
||||
MMHAL_SDIO_FUNCTION_1 = (1ul << 28), /** Function 1 */
|
||||
MMHAL_SDIO_FUNCTION_2 = (2ul << 28), /** Function 2 */
|
||||
};
|
||||
|
||||
/** SDIO CMD53 block mode*/
|
||||
enum mmhal_sdio_mode {
|
||||
MMHAL_SDIO_MODE_BYTE = 0, /** Byte mode */
|
||||
MMHAL_SDIO_MODE_BLOCK = (1ul << 27), /** Block mode */
|
||||
};
|
||||
|
||||
/** SDIO CMD53 OP code */
|
||||
enum mmhal_sdio_opcode {
|
||||
/** Operate on a single, fixed address. */
|
||||
MMHAL_SDIO_OPCODE_FIXED_ADDR = 0,
|
||||
/** Increment address by 1 after each byte. */
|
||||
MMHAL_SDIO_OPCODE_INC_ADDR = (1ul << 26),
|
||||
};
|
||||
|
||||
/** CMD52/53 Register Address (17 bit) offset. */
|
||||
#define MMHAL_SDIO_ADDRESS_OFFSET (9)
|
||||
/** CMD52/53 Register Address maximum value. */
|
||||
#define MMHAL_SDIO_ADDRESS_MAX ((1ul << 18) - 1)
|
||||
|
||||
/** CMD53 Byte/block count offset (9 bit). */
|
||||
#define MMHAL_SDIO_COUNT_OFFSET (0)
|
||||
/**CMD53 Byte/block count maximum value. */
|
||||
#define MMHAL_SDIO_COUNT_MAX ((1ul << 10) - 1)
|
||||
|
||||
/** CMD52 Data (8 bit) offset */
|
||||
#define MMHAL_SDIO_CMD52_DATA_OFFSET (0)
|
||||
|
||||
/**
|
||||
* Construct an SDIO CMD52 argument based on the given arguments.
|
||||
*
|
||||
* @param rw Flag indication direction (read or write).
|
||||
* @param fn The applicable function.
|
||||
* @param address The address to read/write. Must be <= @c MMHAL_SDIO_ADDRESS_MAX.
|
||||
* @param write_data The data to write if this is a write operation. Should be set to zero
|
||||
* for a read operation.
|
||||
*
|
||||
* @return the SDIO CMD52 argument generated based on the given arguments.
|
||||
*/
|
||||
static inline uint32_t mmhal_make_cmd52_arg(enum mmhal_sdio_rw rw, enum mmhal_sdio_function fn, uint32_t address,
|
||||
uint8_t write_data)
|
||||
{
|
||||
uint32_t arg;
|
||||
|
||||
arg = rw | fn;
|
||||
arg |= (address << MMHAL_SDIO_ADDRESS_OFFSET);
|
||||
arg |= (write_data << MMHAL_SDIO_CMD52_DATA_OFFSET);
|
||||
return arg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an SDIO CMD53 argument based on the given arguments.
|
||||
*
|
||||
* @param rw Flag indication direction (read or write).
|
||||
* @param fn The applicable function.
|
||||
* @param mode Selects between byte and block mode.
|
||||
* @param address The address to read/write. Must be <= @c MMHAL_SDIO_ADDRESS_MAX.
|
||||
* @param count The count of bytes/blocks (depending on @p mode) to transfer. Must
|
||||
* be <= @c MMHAL_SDIO_COUNT_MAX.
|
||||
*
|
||||
* @note OP Code 1 (incrementing address) is assumed. See also @ref MMHAL_SDIO_OPCODE_INC_ADDR.
|
||||
*
|
||||
* @return the SDIO CMD53 argument generated based on the given arguments.
|
||||
*/
|
||||
static inline uint32_t mmhal_make_cmd53_arg(enum mmhal_sdio_rw rw, enum mmhal_sdio_function fn, enum mmhal_sdio_mode mode,
|
||||
uint32_t address, uint16_t count)
|
||||
{
|
||||
uint32_t arg;
|
||||
|
||||
arg = rw | fn | MMHAL_SDIO_OPCODE_INC_ADDR | mode;
|
||||
arg |= (address << MMHAL_SDIO_ADDRESS_OFFSET);
|
||||
arg |= (count << MMHAL_SDIO_COUNT_OFFSET);
|
||||
return arg;
|
||||
}
|
||||
|
||||
/** @} */
|
||||
|
||||
/** @} */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
/** @} */
|
||||
@@ -0,0 +1,383 @@
|
||||
/*
|
||||
* Copyright 2023 Morse Micro
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @defgroup MMIPAL Morse Micro IP Stack Abstraction Layer (MMIPAL) API
|
||||
*
|
||||
* This API provides a layer of abstraction from the underlying IP stack for common operations
|
||||
* such as configuring the link and getting link status.
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/** Maximum length of an IP address string, including null-terminator. */
|
||||
#ifndef MMIPAL_IPADDR_STR_MAXLEN
|
||||
#define MMIPAL_IPADDR_STR_MAXLEN (48)
|
||||
#endif
|
||||
|
||||
/** Maximum number of IPv6 addresses supported. */
|
||||
#ifndef MMIPAL_MAX_IPV6_ADDRESSES
|
||||
#define MMIPAL_MAX_IPV6_ADDRESSES (3)
|
||||
#endif
|
||||
|
||||
/** Enumeration of status codes returned by MMIPAL functions. */
|
||||
enum mmipal_status {
|
||||
/** Completed successfully. */
|
||||
MMIPAL_SUCCESS,
|
||||
/** One or more arguments were invalid. */
|
||||
MMIPAL_INVALID_ARGUMENT,
|
||||
/** The operation could not complete because the link is not up. */
|
||||
MMIPAL_NO_LINK,
|
||||
/** Failed due to memory allocation failure. */
|
||||
MMIPAL_NO_MEM,
|
||||
/** This functionality is not supported (e.g., due to build configuration). */
|
||||
MMIPAL_NOT_SUPPORTED,
|
||||
};
|
||||
|
||||
/** Enumeration of link states. */
|
||||
enum mmipal_link_state {
|
||||
/** Link is down. */
|
||||
MMIPAL_LINK_DOWN,
|
||||
/** Link is up. */
|
||||
MMIPAL_LINK_UP,
|
||||
};
|
||||
|
||||
/** Enumeration of IP address allocation modes. */
|
||||
enum mmipal_addr_mode {
|
||||
/** Disabled. */
|
||||
MMIPAL_DISABLED,
|
||||
/** Static IP address. */
|
||||
MMIPAL_STATIC,
|
||||
/** IP address allocated via DHCP. @c LWIP_DHCP must be set to 1 if using LWIP. */
|
||||
MMIPAL_DHCP,
|
||||
/** IP address allocated via AutoIP. @c LWIP_DHCP must be set to 1 if using LWIP. */
|
||||
MMIPAL_AUTOIP,
|
||||
/** DHCP offloaded to chip. */
|
||||
MMIPAL_DHCP_OFFLOAD,
|
||||
};
|
||||
|
||||
/** IP address string type. */
|
||||
typedef char mmipal_ip_addr_t[MMIPAL_IPADDR_STR_MAXLEN];
|
||||
|
||||
/**
|
||||
* IPv4 configuration structure.
|
||||
*
|
||||
* This should be initialized using @c MMIPAL_IP_CONFIG_DEFAULT.
|
||||
* For example:
|
||||
*
|
||||
* @code{.c}
|
||||
* struct mmipal_ip_config config = MMIPAL_IP_CONFIG_DEAFULT;
|
||||
* @endcode
|
||||
*/
|
||||
struct mmipal_ip_config {
|
||||
/** IP address allocation mode. */
|
||||
enum mmipal_addr_mode mode;
|
||||
/** local IP address */
|
||||
mmipal_ip_addr_t ip_addr;
|
||||
/** Netmask address */
|
||||
mmipal_ip_addr_t netmask;
|
||||
/** Gateway address */
|
||||
mmipal_ip_addr_t gateway_addr;
|
||||
};
|
||||
|
||||
/** Initializer for @ref mmipal_ip_config. */
|
||||
#define MMIPAL_IP_CONFIG_DEFAULT \
|
||||
{ \
|
||||
MMIPAL_DHCP, "", "", "", \
|
||||
}
|
||||
|
||||
/** Enumeration of IPv6 address allocation modes. */
|
||||
enum mmipal_ip6_addr_mode {
|
||||
/** Disabled. */
|
||||
MMIPAL_IP6_DISABLED,
|
||||
/** Static IPv6 addresses. */
|
||||
MMIPAL_IP6_STATIC,
|
||||
/** IPv6 address allocated via autoconfiguration.
|
||||
* @c LWIP_IPV6_AUTOCONFIG must be set to 1 if using LWIP. */
|
||||
MMIPAL_IP6_AUTOCONFIG,
|
||||
/** IPv6 address allocated via stateless DHCPv6.
|
||||
* @c LWIP_IPV6_DHCP6_STATELESS must be set to 1 if using LWIP. */
|
||||
MMIPAL_IP6_DHCP6_STATELESS,
|
||||
};
|
||||
|
||||
/**
|
||||
* IPv6 configuration structure.
|
||||
*
|
||||
* This should be initialized using @c MMIPAL_IP6_CONFIG_DEFAULT.
|
||||
* For example:
|
||||
*
|
||||
* @code{.c}
|
||||
* struct mmipal_ip6_config config = MMIPAL_IP6_CONFIG_DEFAULT;
|
||||
* @endcode
|
||||
*/
|
||||
struct mmipal_ip6_config {
|
||||
/** IPv6 addresses allocation mode. */
|
||||
enum mmipal_ip6_addr_mode ip6_mode;
|
||||
/** Array of IPv6 addresses. */
|
||||
mmipal_ip_addr_t ip6_addr[MMIPAL_MAX_IPV6_ADDRESSES];
|
||||
};
|
||||
|
||||
/** Initializer for @ref mmipal_ip6_config. */
|
||||
#define MMIPAL_IP6_CONFIG_DEFAULT \
|
||||
{ \
|
||||
MMIPAL_IP6_AUTOCONFIG \
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize arguments structure.
|
||||
*
|
||||
* This should be initialized using @c MMIPAL_INIT_ARGS_DEFAULT.
|
||||
* For example:
|
||||
*
|
||||
* @code{.c}
|
||||
* struct mmipal_init_args args = MMIPAL_INIT_ARGS_DEFAULT;
|
||||
* @endcode
|
||||
*/
|
||||
struct mmipal_init_args {
|
||||
/** IP address allocation mode to use. */
|
||||
enum mmipal_addr_mode mode;
|
||||
/** IP address to use (if @c mode is @c MMIPAL_STATIC). */
|
||||
mmipal_ip_addr_t ip_addr;
|
||||
/** Netmask to use (if @c mode is @c MMIPAL_STATIC). */
|
||||
mmipal_ip_addr_t netmask;
|
||||
/** Gateway IP address to use (if @c mode is @c MMIPAL_STATIC). */
|
||||
mmipal_ip_addr_t gateway_addr;
|
||||
|
||||
/** IPv6 address allocation mode to use. */
|
||||
enum mmipal_ip6_addr_mode ip6_mode;
|
||||
/** IPv6 address to use (if @c ip6_mode is @c MMIPAL_IP6_STATIC). */
|
||||
mmipal_ip_addr_t ip6_addr;
|
||||
/** Flag requesting ARP response offload feature */
|
||||
bool offload_arp_response;
|
||||
/** ARP refresh offload interval in seconds */
|
||||
uint32_t offload_arp_refresh_s;
|
||||
};
|
||||
|
||||
/**
|
||||
* Default values for @ref mmipal_init_args. This should be used when initializing the
|
||||
* @ref mmipal_init_args structure.
|
||||
*/
|
||||
#define MMIPAL_INIT_ARGS_DEFAULT \
|
||||
{ \
|
||||
MMIPAL_DHCP, {0}, {0}, {0}, MMIPAL_IP6_DISABLED, {0}, false, 0 \
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the IP stack and enable the MMWLAN interface.
|
||||
*
|
||||
* This will implicitly initialize and boot MMWLAN, and will block until this has completed.
|
||||
*
|
||||
* @note This function will boot the Morse Micro transceiver using @ref mmwlan_boot() in order
|
||||
* to read the MAC address. It is the responsibility of the caller to shut down the
|
||||
* transceiver using @ref mmwlan_shutdown() as required.
|
||||
*
|
||||
* @warning @ref mmwlan_init() must be called before invoking this function.
|
||||
*
|
||||
* @param args Initialization arguments.
|
||||
*
|
||||
* @return @c MMIPAL_SUCCESS on success. otherwise a vendor specific error code.
|
||||
*/
|
||||
enum mmipal_status mmipal_init(const struct mmipal_init_args *args);
|
||||
|
||||
/**
|
||||
* Structure representing the current status of the link.
|
||||
*/
|
||||
struct mmipal_link_status {
|
||||
/** State of the link (up/down). */
|
||||
enum mmipal_link_state link_state;
|
||||
/** Current IP address. */
|
||||
mmipal_ip_addr_t ip_addr;
|
||||
/** Current netmask. */
|
||||
mmipal_ip_addr_t netmask;
|
||||
/** Current gateway IP address. */
|
||||
mmipal_ip_addr_t gateway;
|
||||
};
|
||||
|
||||
/**
|
||||
* Prototype for callback function invoked on link status changes.
|
||||
*
|
||||
* @param link_status The current link status.
|
||||
*/
|
||||
typedef void (*mmipal_link_status_cb_fn_t)(const struct mmipal_link_status *link_status);
|
||||
|
||||
/**
|
||||
* Sets the callback function to be invoked on link status changes.
|
||||
*
|
||||
* This will be used when DHCP is enabled.
|
||||
*
|
||||
* @note This is for IPv4 only. To get IPv6 status use @c mmipal_get_ip6_config.
|
||||
* @note If an opaque argument is required then use @ref mmipal_set_ext_link_status_callback()
|
||||
* instead.
|
||||
*
|
||||
* @param fn Function pointer to the callback function.
|
||||
*/
|
||||
void mmipal_set_link_status_callback(mmipal_link_status_cb_fn_t fn);
|
||||
|
||||
/**
|
||||
* Prototype for callback function invoked on link status changes.
|
||||
*
|
||||
* This is similar to @ref mmipal_link_status_cb_fn_t but with the addition of the @p arg
|
||||
* parameter.
|
||||
*
|
||||
* @param link_status The current link status.
|
||||
* @param arg Opaque argument that was provided when the callback was registered.
|
||||
*/
|
||||
typedef void (*mmipal_ext_link_status_cb_fn_t)(const struct mmipal_link_status *link_status, void *arg);
|
||||
|
||||
/**
|
||||
* Sets the extended link status callback function to be invoked on link status changes.
|
||||
* This is similar to @ref mmipal_set_link_status_callback() with the exception that
|
||||
* an opaque argument may also be specified.
|
||||
*
|
||||
* This will be used when DHCP is enabled.
|
||||
*
|
||||
* @note This is for IPv4 only. To get IPv6 status use @c mmipal_get_ip6_config.
|
||||
*
|
||||
* @param fn Function pointer to the callback function.
|
||||
* @param arg Opaque argument to be passed to the callback.
|
||||
*/
|
||||
void mmipal_set_ext_link_status_callback(mmipal_ext_link_status_cb_fn_t fn, void *arg);
|
||||
|
||||
/**
|
||||
* Get the total number of transmitted and received packets on the MMWLAN interface
|
||||
*
|
||||
* @note If using LWIP, this function requires LWIP_STATS to be defined in your application,
|
||||
* otherwise packet counters will always return as 0.
|
||||
*
|
||||
* @param tx_packets Pointer to location to store total tx packets
|
||||
* @param rx_packets Pointer to location to store total rx packets
|
||||
*/
|
||||
void mmipal_get_link_packet_counts(uint32_t *tx_packets, uint32_t *rx_packets);
|
||||
|
||||
/**
|
||||
* Set the QoS Traffic ID to use when transmitting.
|
||||
*
|
||||
* @param tid The QoS TID to use (0 - @ref MMWLAN_MAX_QOS_TID).
|
||||
*/
|
||||
void mmipal_set_tx_qos_tid(uint8_t tid);
|
||||
|
||||
/**
|
||||
* Gets the local address for the MMWLAN interface that is appropriate for a given
|
||||
* destination address.
|
||||
*
|
||||
* The following table shows how the returned @c local_addr is selected:
|
||||
*
|
||||
* | @p dest_addr | @c local_addr returned |
|
||||
* |--------------|---------------------------|
|
||||
* | type is IPv4 | IPv4 address |
|
||||
* | type is IPv6 | An IPv6 source address selected from interface's IPv6 addresses or ERR_CONN |
|
||||
*
|
||||
* (X = don't care)
|
||||
*
|
||||
* If the given parameters would result in a @p local_addr type of IPv4 and IPv4 is not enabled,
|
||||
* or IPv6 and IPv6 is not enabled, then @c MMIPAL_INVALID_ARGUMENT will be returned.
|
||||
*
|
||||
* @param[out] local_addr Output local address for the MMWLAN interface, as noted above.
|
||||
* @param[in] dest_addr Destination address.
|
||||
*
|
||||
* @return @c MMIPAL_SUCESS if @p local_addr successfully set. otherwise an
|
||||
* appropriate error code.
|
||||
*/
|
||||
enum mmipal_status mmipal_get_local_addr(mmipal_ip_addr_t local_addr, const mmipal_ip_addr_t dest_addr);
|
||||
|
||||
/**
|
||||
* Get the IP configurations.
|
||||
*
|
||||
* This can be used to get the local IP configurations.
|
||||
*
|
||||
* @param config Pointer to the IP configurations.
|
||||
*
|
||||
* @returns @c MMIPAL_SUCCESS on success, @c MMIPAL_NOT_SUPPORTED if IPv4 is not supported.
|
||||
*/
|
||||
enum mmipal_status mmipal_get_ip_config(struct mmipal_ip_config *config);
|
||||
|
||||
/**
|
||||
* Set the IP configurations.
|
||||
*
|
||||
* This can be used to set the local IP configurations.
|
||||
*
|
||||
* @param config Pointer to the IP configurations.
|
||||
*
|
||||
* @returns @c MMIPAL_SUCCESS on success, @c MMIPAL_NOT_SUPPORTED if IPv4 is not supported.
|
||||
*/
|
||||
enum mmipal_status mmipal_set_ip_config(const struct mmipal_ip_config *config);
|
||||
|
||||
/**
|
||||
* Gets the current IPv4 broadcast address.
|
||||
*
|
||||
* @param[out] broadcast_addr Buffer to receive the broadcast address as a string.
|
||||
*
|
||||
* @returns @c MMIPAL_SUCCESS on success, @c MMIPAL_NOT_SUPPORTED if IPv4 is not supported.
|
||||
*/
|
||||
enum mmipal_status mmipal_get_ip_broadcast_addr(mmipal_ip_addr_t broadcast_addr);
|
||||
|
||||
/**
|
||||
* Get the IP configurations.
|
||||
*
|
||||
* This can be used to get the local IP configurations.
|
||||
*
|
||||
* @param config Pointer to the IP configurations.
|
||||
*
|
||||
* @returns @c MMIPAL_SUCCESS on success, @c MMIPAL_NOT_SUPPORTED if IPv6 is not supported..
|
||||
*/
|
||||
enum mmipal_status mmipal_get_ip6_config(struct mmipal_ip6_config *config);
|
||||
|
||||
/**
|
||||
* Set the IPv6 configurations.
|
||||
*
|
||||
* This can be used to set the local IPv6 configurations.
|
||||
*
|
||||
* @param config Pointer to the IPv6 configurations.
|
||||
*
|
||||
* @returns @c MMIPAL_SUCCESS on success, @c MMIPAL_NOT_SUPPORTED if IPv6 is not supported.
|
||||
*/
|
||||
enum mmipal_status mmipal_set_ip6_config(const struct mmipal_ip6_config *config);
|
||||
|
||||
/**
|
||||
* Get current IPv4 link state.
|
||||
*
|
||||
* @returns the current IPv4 link state (up or down).
|
||||
*/
|
||||
enum mmipal_link_state mmipal_get_link_state(void);
|
||||
|
||||
/**
|
||||
* Set the DNS server at the given index.
|
||||
*
|
||||
* @warning Depending on IP stack implementation, this setting may be overridden by DHCP.
|
||||
*
|
||||
* @param[in] index Index of the DNS server to set.
|
||||
* @param[out] addr Address of the DNS server to set.
|
||||
*
|
||||
* @returns @c MMIPAL_SUCCESS on success, @c MMIPAL_INVALID_ARGUMENT if an invalid index or IP
|
||||
* address was given.
|
||||
*/
|
||||
enum mmipal_status mmipal_set_dns_server(uint8_t index, const mmipal_ip_addr_t addr);
|
||||
|
||||
/**
|
||||
* Get the DNS server at the given index.
|
||||
*
|
||||
* @param[in] index Index of the DNS server to set.
|
||||
* @param[out] addr IP address buffer to receive the IP address of the DNS server at the given
|
||||
* index. Will be set to empty string if no server at the given index.
|
||||
*
|
||||
* @returns @c MMIPAL_SUCCESS on success.
|
||||
*/
|
||||
enum mmipal_status mmipal_get_dns_server(uint8_t index, mmipal_ip_addr_t addr);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
/** @} */
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Morse logging API
|
||||
*
|
||||
* Copyright 2023 Morse Micro
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
* Macro for printing a @c uint64_t as two separate @c uint32_t values. This is to allow printing of
|
||||
* these values even when the @c printf implementation doesn't support it.
|
||||
*/
|
||||
#define MM_X64_VAL(value) ((uint32_t)(value >> 32)), ((uint32_t)value)
|
||||
|
||||
/** Macro for format specifier to print @ref MM_X64_VAL */
|
||||
#define MM_X64_FMT "%08lx%08lx"
|
||||
|
||||
/**
|
||||
* Macro for printing a MAC address. This saves writing it out by hand.
|
||||
*
|
||||
* Must be used in conjunction with @ref MM_MAC_ADDR_FMT. For example:
|
||||
*
|
||||
* @code
|
||||
* uint8_t mac_addr[] = { 0, 1, 2, 3, 4, 5 };
|
||||
* printf("MAC address: " MM_MAC_ADDR_FMT "\n", MM_MAC_ADDR_VAL(mac_addr));
|
||||
* @endcode
|
||||
*/
|
||||
#define MM_MAC_ADDR_VAL(value) ((value)[0]), ((value)[1]), ((value)[2]), ((value)[3]), ((value)[4]), ((value)[5])
|
||||
|
||||
/** Macro for format specifier to print @ref MM_MAC_ADDR_VAL */
|
||||
#define MM_MAC_ADDR_FMT "%02x:%02x:%02x:%02x:%02x:%02x"
|
||||
|
||||
/**
|
||||
* Initialize Morse logging API.
|
||||
*
|
||||
* This should be invoked after OS initialization since it will create a mutex for
|
||||
* logging.
|
||||
*/
|
||||
void mm_logging_init(void);
|
||||
|
||||
/**
|
||||
* Dumps a binary buffer in hex.
|
||||
*
|
||||
* @param level A single character indicating log level.
|
||||
* @param function Name of function this was invoked from.
|
||||
* @param line_number Line number this was invoked from.
|
||||
* @param title Title of the buffer.
|
||||
* @param buf The buffer to dump.
|
||||
* @param len Length of the buffer.
|
||||
*/
|
||||
void mm_hexdump(char level, const char *function, unsigned line_number, const char *title, const uint8_t *buf, size_t len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,515 @@
|
||||
/*
|
||||
* Copyright 2022-2024 Morse Micro
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @defgroup MMPKT Morse Micro Packet Buffer (mmpkt) API
|
||||
*
|
||||
* This API provides support for buffers tailored towards packets.
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "mmosal.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Round @p x up to the next multiple of @p m (where @p m is a power of 2).
|
||||
*
|
||||
* @warning @p m must be a power of 2.
|
||||
*/
|
||||
#ifndef MM_FAST_ROUND_UP
|
||||
#define MM_FAST_ROUND_UP(x, m) ((((x)-1) | ((m)-1)) + 1)
|
||||
#endif
|
||||
|
||||
struct mmdrv_cmd_metadata;
|
||||
struct mmdrv_tx_metadata;
|
||||
struct mmdrv_rx_metadata;
|
||||
struct mmpkt_ops;
|
||||
|
||||
/**
|
||||
* Union of pointer types for mmpkt metadata.
|
||||
*
|
||||
* The metadata is accessed through one of these pointers, depending on which context the packet
|
||||
* is being used in.
|
||||
*/
|
||||
union mmpkt_metadata_ptr {
|
||||
/** Opaque pointer for contexts which are unaware of the specific metadata structure. */
|
||||
void *opaque;
|
||||
/** Metadata for a packet which is being transmitted. */
|
||||
struct mmdrv_tx_metadata *tx;
|
||||
/** Metadata for a packet which is being received. */
|
||||
struct mmdrv_rx_metadata *rx;
|
||||
/** Control block for a command response sent to the host. */
|
||||
struct mmdrv_cmd_metadata *cmd;
|
||||
};
|
||||
|
||||
/**
|
||||
* Core mmpkt data structure.
|
||||
*
|
||||
* @note The contents of this data structure should never need to be accessed directly. Rather
|
||||
* the various functions provided as part of this API should be used.
|
||||
*
|
||||
* @code
|
||||
* +----------------------------------------------------------+--------------+
|
||||
* | RESERVED | Data | RESERVED | METADATA |
|
||||
* +----------------------------------------------------------+--------------+
|
||||
* ^ ^ ^ ^
|
||||
* | | | |
|
||||
* | |<-----------data_len--------->| |
|
||||
* | start_offset |
|
||||
* | |
|
||||
* |<-----------------------buf_len-------------------------->|
|
||||
* buf
|
||||
* @endcode
|
||||
*/
|
||||
struct mmpkt {
|
||||
/** The buffer where data is stored. */
|
||||
uint8_t *buf;
|
||||
/** Length of the buffer. */
|
||||
uint32_t buf_len;
|
||||
/** Offset where actual data starts in the buffer. */
|
||||
uint32_t start_offset;
|
||||
/** Length of actual data in the buffer. */
|
||||
uint32_t data_len;
|
||||
/** Packet metadata used by driver (context dependent). */
|
||||
union mmpkt_metadata_ptr metadata;
|
||||
/** Reference to operations data structure for this mmpkt. */
|
||||
const struct mmpkt_ops *ops;
|
||||
/** Pointer that can be used to construct linked lists. */
|
||||
struct mmpkt *volatile next;
|
||||
};
|
||||
|
||||
/** Operations data structure for mmpkt. */
|
||||
struct mmpkt_ops {
|
||||
/** Free the given mmpkt. */
|
||||
void (*free_mmpkt)(void *mmpkt);
|
||||
};
|
||||
|
||||
/**
|
||||
* Opened view of an mmpkt.
|
||||
*
|
||||
* In this implementation, this structure does not actually exist. We only use it as a pointer type
|
||||
* which is incompatible with @ref mmpkt, to distinguish between functions which operate on opened
|
||||
* packet views versus functions which can operate on unopened packets.
|
||||
*
|
||||
* In other implementations of this API, packets must be "opened" (mapped into memory) before their
|
||||
* contents can be accessed. Thus the distinction between opened and unopened packets is important
|
||||
* for those implementations.
|
||||
*/
|
||||
struct mmpktview;
|
||||
|
||||
/**
|
||||
* Initialize an mmpkt header with the given values.
|
||||
*
|
||||
* @param mmpkt mmpkt to initialize.
|
||||
* @param buf Pointer to buffer.
|
||||
* @param buf_len Length of @p buf.
|
||||
* @param data_start_offset Initial value for @c start_offset.
|
||||
* @param ops Operations data structure.
|
||||
*/
|
||||
static inline void mmpkt_init(struct mmpkt *mmpkt, uint8_t *buf, uint32_t buf_len, uint32_t data_start_offset,
|
||||
const struct mmpkt_ops *ops)
|
||||
{
|
||||
memset(mmpkt, 0, sizeof(*mmpkt));
|
||||
mmpkt->buf = buf;
|
||||
mmpkt->buf_len = buf_len;
|
||||
mmpkt->start_offset = data_start_offset;
|
||||
mmpkt->ops = ops;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize an mmpkt in a single buffer using the given values.
|
||||
*
|
||||
* @param buf Pointer to buffer.
|
||||
* @param buf_len Length of @p buf.
|
||||
* @param space_at_start Amount of space to reserve at start of buffer.
|
||||
* @param space_at_end Amount of space to reserve at end of buffer.
|
||||
* @param metadata_size Size of metadata (0 for no metadata).
|
||||
*
|
||||
* @param ops Operations data structure.
|
||||
*
|
||||
* @note @p buf_len must be large enough to contain the @c mmkpt header, the data buffer
|
||||
* (rounded up to the nearest 4 bytes) and metadata (rounded up to the nearest 4 bytes).
|
||||
*
|
||||
* @returns a pointer to the initialized @c mmpkt (will be the same address as @p buf) or @c NULL
|
||||
* on error (@p buf length too short).
|
||||
*/
|
||||
static inline struct mmpkt *mmpkt_init_buf(uint8_t *buf, uint32_t buf_len, uint32_t space_at_start, uint32_t space_at_end,
|
||||
uint32_t metadata_size, const struct mmpkt_ops *ops)
|
||||
{
|
||||
struct mmpkt *mmpkt = (struct mmpkt *)buf;
|
||||
|
||||
uint8_t *data_start;
|
||||
uint32_t header_size = MM_FAST_ROUND_UP(sizeof(*mmpkt), 4);
|
||||
uint32_t data_len = MM_FAST_ROUND_UP(space_at_start + space_at_end, 4);
|
||||
metadata_size = MM_FAST_ROUND_UP(metadata_size, 4);
|
||||
|
||||
if (header_size + data_len + metadata_size > buf_len) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
data_start = ((uint8_t *)mmpkt) + header_size;
|
||||
|
||||
mmpkt_init(mmpkt, data_start, data_len, space_at_start, ops);
|
||||
|
||||
if (metadata_size != 0) {
|
||||
mmpkt->metadata.opaque = data_start + data_len;
|
||||
memset(mmpkt->metadata.opaque, 0, metadata_size);
|
||||
}
|
||||
|
||||
return mmpkt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocate a new mmpkt on the heap (using @ref mmosal_malloc()).
|
||||
*
|
||||
* @param space_at_start Amount of space to reserve at start of buffer.
|
||||
* @param space_at_end Amount of space to reserve at end of buffer.
|
||||
* @param metadata_size Size of metadata (0 for no metadata).
|
||||
*
|
||||
* @note @c start_offset will be set to @p space_at_start, and @c buf_len will be the sum
|
||||
* of @p space_at_start and @p space_at_end (rounded up to a multiple of 4).
|
||||
*
|
||||
* @returns newly allocated mmpkt on success or @c NULL on failure.
|
||||
*/
|
||||
struct mmpkt *mmpkt_alloc_on_heap(uint32_t space_at_start, uint32_t space_at_end, uint32_t metadata_size);
|
||||
|
||||
/**
|
||||
* Release a reference to the given mmpkt. If this was the last reference (@c addition_ref_cnt
|
||||
* was 0) then the mmpkt will be freed using the appropriate op callback.
|
||||
*
|
||||
* @param mmpkt The mmpkt to release reference to. May be @c NULL.
|
||||
*/
|
||||
void mmpkt_release(struct mmpkt *mmpkt);
|
||||
|
||||
/**
|
||||
* Open a view of the given mmpkt.
|
||||
*
|
||||
* Packets must be opened before the contents of their buffer can be accessed. Most of the
|
||||
* functions below expect to be passed an opened view of a packet to operate on.
|
||||
*
|
||||
* The view must be closed by calling @ref mmpkt_close() before the packet is released by
|
||||
* @ref mmpkt_release().
|
||||
*
|
||||
* @param mmpkt The mmpkt to be opened.
|
||||
*
|
||||
* @returns a pointer representing the opened view.
|
||||
*/
|
||||
static inline struct mmpktview *mmpkt_open(struct mmpkt *mmpkt)
|
||||
{
|
||||
return (struct mmpktview *)mmpkt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the given view.
|
||||
*
|
||||
* @param[in,out] view Pointer to a variable holding the view to be closed.
|
||||
* This will be modified to indicate it is no longer valid.
|
||||
*/
|
||||
static inline void mmpkt_close(struct mmpktview **view)
|
||||
{
|
||||
(void)(view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying mmpkt from an opened view.
|
||||
*
|
||||
* @param view View of an mmpkt.
|
||||
*
|
||||
* @returns the underlying mmpkt.
|
||||
*/
|
||||
static inline struct mmpkt *mmpkt_from_view(struct mmpktview *view)
|
||||
{
|
||||
return (struct mmpkt *)view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a pointer to the start of the data in the mmpkt.
|
||||
*
|
||||
* @param view The opened mmpkt to operate on.
|
||||
*
|
||||
* @returns a pointer to the start of the data in the mmpkt.
|
||||
*/
|
||||
static inline uint8_t *mmpkt_get_data_start(struct mmpktview *view)
|
||||
{
|
||||
struct mmpkt *mmpkt = (struct mmpkt *)view;
|
||||
return mmpkt->buf + mmpkt->start_offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a pointer to the end of the data in the mmpkt.
|
||||
*
|
||||
* @param view The opened mmpkt to operate on.
|
||||
*
|
||||
* @returns a pointer to the end of the data in the mmpkt.
|
||||
*/
|
||||
static inline uint8_t *mmpkt_get_data_end(struct mmpktview *view)
|
||||
{
|
||||
struct mmpkt *mmpkt = (struct mmpkt *)view;
|
||||
return mmpkt->buf + mmpkt->start_offset + mmpkt->data_len;
|
||||
}
|
||||
|
||||
/**
|
||||
* Peek the length of the data currently from an unopened mmpkt.
|
||||
*
|
||||
* @param mmpkt The unopened mmpkt to operate on.
|
||||
*
|
||||
* @returns the length of the data currently in the mmpkt (note that this is different from the
|
||||
* length of the available buffer space).
|
||||
*/
|
||||
static inline uint32_t mmpkt_peek_data_length(struct mmpkt *mmpkt)
|
||||
{
|
||||
return mmpkt->data_len;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the length of the data currently in the mmpkt.
|
||||
*
|
||||
* @param view The opened mmpkt to operate on.
|
||||
*
|
||||
* @returns the length of the data currently in the mmpkt (note that this is different from the
|
||||
* length of the available buffer space).
|
||||
*/
|
||||
static inline uint32_t mmpkt_get_data_length(struct mmpktview *view)
|
||||
{
|
||||
struct mmpkt *mmpkt = (struct mmpkt *)view;
|
||||
return mmpkt->data_len;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount of space available for prepending to the data in the buffer.
|
||||
*
|
||||
* @param view The opened mmpkt to operate on.
|
||||
*
|
||||
* @returns the available space in bytes.
|
||||
*/
|
||||
static inline uint32_t mmpkt_available_space_at_start(struct mmpktview *view)
|
||||
{
|
||||
struct mmpkt *mmpkt = (struct mmpkt *)view;
|
||||
return mmpkt->start_offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount of space available for appending to the data in the buffer.
|
||||
*
|
||||
* @param view The opened mmpkt to operate on.
|
||||
*
|
||||
* @returns the available space in bytes.
|
||||
*/
|
||||
static inline uint32_t mmpkt_available_space_at_end(struct mmpktview *view)
|
||||
{
|
||||
struct mmpkt *mmpkt = (struct mmpkt *)view;
|
||||
return mmpkt->buf_len - (mmpkt->start_offset + mmpkt->data_len);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reserves space immediately before the data currently in the given mmpkt and returns
|
||||
* a pointer to this space.
|
||||
*
|
||||
* For a function that also copies data in, see @ref mmpkt_prepend_data().
|
||||
*
|
||||
* @warning @p len must be less than or equal to @ref mmpkt_available_space_at_start().
|
||||
*
|
||||
* @param view The opened mmpkt to operate on.
|
||||
* @param len Length of data to be prepended.
|
||||
*
|
||||
* @returns a pointer to the place in the buffer where the data should be put.
|
||||
*/
|
||||
static inline uint8_t *mmpkt_prepend(struct mmpktview *view, uint32_t len)
|
||||
{
|
||||
struct mmpkt *mmpkt = (struct mmpkt *)view;
|
||||
MMOSAL_ASSERT(len <= mmpkt_available_space_at_start(view));
|
||||
mmpkt->start_offset -= len;
|
||||
mmpkt->data_len += len;
|
||||
return mmpkt->buf + mmpkt->start_offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepends the given data to the data already in the mmpkt.
|
||||
*
|
||||
* @warning @p len must be less than or equal to @ref mmpkt_available_space_at_start().
|
||||
*
|
||||
* @warning The memory area pointed to by data must not overlap with the mmpkt data.
|
||||
*
|
||||
* @param view The opened mmpkt to operate on.
|
||||
* @param data The data to be prepended.
|
||||
* @param len Length of data to be prepended.
|
||||
*/
|
||||
static inline void mmpkt_prepend_data(struct mmpktview *view, const uint8_t *data, uint32_t len)
|
||||
{
|
||||
uint8_t *dest = mmpkt_prepend(view, len);
|
||||
memcpy(dest, data, len);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reserves space immediately after the data currently in the given mmpkt and returns
|
||||
* a pointer to this space.
|
||||
*
|
||||
* For a function that also copies data in, see @ref mmpkt_append_data().
|
||||
*
|
||||
* @warning @p len must be less than or equal to @ref mmpkt_available_space_at_end().
|
||||
*
|
||||
* @param view The opened mmpkt to operate on.
|
||||
* @param len Length of data to be append.
|
||||
*
|
||||
* @returns a pointer to the place in the buffer where the data should be put.
|
||||
*/
|
||||
static inline uint8_t *mmpkt_append(struct mmpktview *view, uint32_t len)
|
||||
{
|
||||
struct mmpkt *mmpkt = (struct mmpkt *)view;
|
||||
uint8_t *ret = mmpkt_get_data_end(view);
|
||||
MMOSAL_ASSERT(len <= mmpkt_available_space_at_end(view));
|
||||
mmpkt->data_len += len;
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the given data to the data already in the mmpkt.
|
||||
*
|
||||
* @warning @p len must be less than or equal to @ref mmpkt_available_space_at_start().
|
||||
*
|
||||
* @param view The opened mmpkt to operate on.
|
||||
* @param data The data to be prepended.
|
||||
* @param len Length of data to be prepended.
|
||||
*/
|
||||
static inline void mmpkt_append_data(struct mmpktview *view, const uint8_t *data, uint32_t len)
|
||||
{
|
||||
uint8_t *dest = mmpkt_append(view, len);
|
||||
memcpy(dest, data, len);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a reference to the metadata associated with the given mmpkt.
|
||||
*
|
||||
* @param mmpkt The mmpkt to operate on.
|
||||
*
|
||||
* @returns a reference to the mmpkt metadata.
|
||||
*/
|
||||
static inline union mmpkt_metadata_ptr mmpkt_get_metadata(struct mmpkt *mmpkt)
|
||||
{
|
||||
return mmpkt->metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove data from the start of the mmpkt.
|
||||
*
|
||||
* @param view The opened mmpkt to operate on.
|
||||
* @param len Length of data to remove.
|
||||
*
|
||||
* @returns a pointer to the removed data or NULL if the mmpkt data length was less than @p len.
|
||||
*/
|
||||
static inline uint8_t *mmpkt_remove_from_start(struct mmpktview *view, uint32_t len)
|
||||
{
|
||||
struct mmpkt *mmpkt = (struct mmpkt *)view;
|
||||
uint8_t *ret;
|
||||
|
||||
if (mmpkt_get_data_length(view) < len) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ret = mmpkt_get_data_start(view);
|
||||
|
||||
mmpkt->start_offset += len;
|
||||
mmpkt->data_len -= len;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove data from the end of the mmpkt.
|
||||
*
|
||||
* @param view The opened mmpkt to operate on.
|
||||
* @param len Length of data to remove.
|
||||
*
|
||||
* @returns a pointer to the removed data or NULL if the mmpkt data length was less than @p len.
|
||||
*/
|
||||
static inline uint8_t *mmpkt_remove_from_end(struct mmpktview *view, uint32_t len)
|
||||
{
|
||||
struct mmpkt *mmpkt = (struct mmpkt *)view;
|
||||
uint8_t *ret;
|
||||
|
||||
if (mmpkt_get_data_length(view) < len) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ret = mmpkt_get_data_end(view) - len;
|
||||
|
||||
mmpkt->data_len -= len;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate the mmpkt data to the given length.
|
||||
*
|
||||
* @param mmpkt mmpkt to operate on.
|
||||
* @param len New data length. (Must be less than or equal to the data length
|
||||
* of the mmpkt).
|
||||
*/
|
||||
static inline void mmpkt_truncate(struct mmpkt *mmpkt, uint32_t len)
|
||||
{
|
||||
MMOSAL_ASSERT(len <= mmpkt->data_len);
|
||||
mmpkt->data_len = len;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the `next` pointer embedded in the mmpkt.
|
||||
*
|
||||
* Used by the mmpkt_list structure for making linked lists of mmpkts.
|
||||
*
|
||||
* @param mmpkt The mmpkt to operate on.
|
||||
*
|
||||
* @returns pointer to the next mmpkt in the chain (may be NULL).
|
||||
*/
|
||||
static inline struct mmpkt *mmpkt_get_next(struct mmpkt *mmpkt)
|
||||
{
|
||||
return mmpkt->next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the `next` pointer embedded in the mmpkt.
|
||||
*
|
||||
* Used by the mmpkt_list structure for making linked lists of mmpkts.
|
||||
*
|
||||
* @param mmpkt The mmpkt to operate on.
|
||||
* @param next The next mmpkt in the chain.
|
||||
*/
|
||||
static inline void mmpkt_set_next(struct mmpkt *mmpkt, struct mmpkt *next)
|
||||
{
|
||||
mmpkt->next = next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given pointer is pointing inside the mmpkt's buffer.
|
||||
*
|
||||
* @note This checks against the full buffer, which includes any unused regions at the beginning and
|
||||
* end of the buffer which do not contain valid packet data.
|
||||
*
|
||||
* @param view The opened mmpkt to operate on.
|
||||
* @param ptr The pointer to check.
|
||||
*
|
||||
* @returns true if the pointer points into the buffer.
|
||||
*/
|
||||
static inline bool mmpkt_contains_ptr(struct mmpktview *view, const void *ptr)
|
||||
{
|
||||
struct mmpkt *mmpkt = (struct mmpkt *)view;
|
||||
return ((const uint8_t *)ptr >= &mmpkt->buf[0] && (const uint8_t *)ptr < &mmpkt->buf[mmpkt->buf_len]);
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
/** @} */
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* Copyright 2022-2024 Morse Micro
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @ingroup MMPKT
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include "mmpkt.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Structure that can be used as the head of a linked list of mmpkts that counts its length. */
|
||||
struct mmpkt_list {
|
||||
/** First mmpkt in the list. */
|
||||
struct mmpkt *volatile head;
|
||||
/** Last mmpkt in the list. */
|
||||
struct mmpkt *volatile tail;
|
||||
/** Length of the list. */
|
||||
volatile uint32_t len;
|
||||
};
|
||||
|
||||
/** Static initializer for @ref mmpkt_list. */
|
||||
#define MMPKT_LIST_INIT \
|
||||
{ \
|
||||
NULL, NULL, 0 \
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialization function for @ref mmpkt_list, for cases where @c MMPKT_LIST_INIT
|
||||
* cannot be used.
|
||||
*
|
||||
* @param list The mmpkt_list to init.
|
||||
*/
|
||||
static inline void mmpkt_list_init(struct mmpkt_list *list)
|
||||
{
|
||||
list->head = NULL;
|
||||
list->tail = NULL;
|
||||
list->len = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an mmpkt to the start of an mmpkt list.
|
||||
*
|
||||
* @param list The list to prepend to.
|
||||
* @param mmpkt The mmpkt to prepend.
|
||||
*/
|
||||
void mmpkt_list_prepend(struct mmpkt_list *list, struct mmpkt *mmpkt);
|
||||
|
||||
/**
|
||||
* Add an mmpkt to the end of an mmpkt list.
|
||||
*
|
||||
* @param list The list to append to.
|
||||
* @param mmpkt The mmpkt to append.
|
||||
*/
|
||||
void mmpkt_list_append(struct mmpkt_list *list, struct mmpkt *mmpkt);
|
||||
|
||||
/**
|
||||
* Remove an mmpkt from an mmpkt list.
|
||||
*
|
||||
* @param list The list to remove from.
|
||||
* @param mmpkt The mmpkt to remove.
|
||||
*/
|
||||
void mmpkt_list_remove(struct mmpkt_list *list, struct mmpkt *mmpkt);
|
||||
|
||||
/**
|
||||
* Remove the mmpkt at the head of the list and return it.
|
||||
*
|
||||
* @param list The list to dequeue from.
|
||||
*
|
||||
* @returns the dequeued mmpkt, or @c NULL if the list is empty.
|
||||
*/
|
||||
struct mmpkt *mmpkt_list_dequeue(struct mmpkt_list *list);
|
||||
|
||||
/**
|
||||
* Remove the mmpkt at the tail of the list and return it.
|
||||
*
|
||||
* @param list The list to dequeue from.
|
||||
*
|
||||
* @returns the dequeued mmpkt, or @c NULL if the list is empty.
|
||||
*/
|
||||
struct mmpkt *mmpkt_list_dequeue_tail(struct mmpkt_list *list);
|
||||
|
||||
/**
|
||||
* Remove all mmpkts from the list and return as a linked list.
|
||||
*
|
||||
* @param list The list to dequeue from.
|
||||
*
|
||||
* @returns the dequeued mmpkts, or @c NULL if the list is empty.
|
||||
*/
|
||||
static inline struct mmpkt *mmpkt_list_dequeue_all(struct mmpkt_list *list)
|
||||
{
|
||||
struct mmpkt *head = list->head;
|
||||
list->head = NULL;
|
||||
list->tail = NULL;
|
||||
list->len = 0;
|
||||
return head;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given mmpkt list is empty.
|
||||
*
|
||||
* @param list The list to check.
|
||||
*
|
||||
* @returns @c true if the list is empty, else @c false.
|
||||
*/
|
||||
static inline bool mmpkt_list_is_empty(struct mmpkt_list *list)
|
||||
{
|
||||
return (list->head == NULL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the head of the mmpkt list.
|
||||
*
|
||||
* @param list The list to peek into.
|
||||
*
|
||||
* @returns the mmpkt at the head of the list.
|
||||
*/
|
||||
static inline struct mmpkt *mmpkt_list_peek(struct mmpkt_list *list)
|
||||
{
|
||||
return list->head;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tail of the mmpkt list.
|
||||
*
|
||||
* @param list The list to peek into.
|
||||
*
|
||||
* @returns the mmpkt at the tail of the list.
|
||||
*/
|
||||
static inline struct mmpkt *mmpkt_list_peek_tail(struct mmpkt_list *list)
|
||||
{
|
||||
return list->tail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Free all the packets in the given list and reset the list to empty state.
|
||||
*
|
||||
* @param list The list to clear.
|
||||
*/
|
||||
void mmpkt_list_clear(struct mmpkt_list *list);
|
||||
|
||||
/**
|
||||
* Safely walk the mmpkt list.
|
||||
*
|
||||
* @warning This macro cannot be used following an if statement with no parentheses if there
|
||||
* is an else clause. For example, do not do:
|
||||
* `if (x) MMPKT_LIST_WALK(a,b,c) else foo();` -- instead:
|
||||
* `if (x) { MMPKT_LIST_WALK(a,b,c) } else foo();`
|
||||
*/
|
||||
#define MMPKT_LIST_WALK(_lst, _wlk, _nxt) \
|
||||
if ((_lst)->head != NULL) /* NOLINT(readability/braces) */ \
|
||||
for (_wlk = (_lst)->head, _nxt = mmpkt_get_next(_wlk); _wlk != NULL; \
|
||||
_wlk = _nxt, _nxt = _wlk ? mmpkt_get_next(_wlk) : NULL)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,302 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2022-2023 Morse Micro
|
||||
*/
|
||||
/**
|
||||
* @ingroup MMWLAN_REGDB
|
||||
* @defgroup MMWLAN_REGDB_TEMPLATE Template S1G regulatory database
|
||||
*
|
||||
* \{
|
||||
*
|
||||
* @section MMWLAN_REGDB_TEMPLATE_DISCLAIMER Disclaimer
|
||||
*
|
||||
* While every effort has been made to maintain accuracy of this database, no guarantee is
|
||||
* given as to the accuracy of the information contained herein.
|
||||
*
|
||||
* @section MMWLAN_REGDB_TEMPLATE_COUNTRIES Country code list
|
||||
*
|
||||
* | Country Code | Country |
|
||||
* | ------------ | ------- |
|
||||
* | AU | Australia |
|
||||
* | EU | EU |
|
||||
* | IN | India |
|
||||
* | JP | Japan |
|
||||
* | KR | South Korea |
|
||||
* | NZ | New Zealand |
|
||||
* | SG | Singapore |
|
||||
* | US | USA |
|
||||
*/
|
||||
|
||||
#include "mmwlan.h"
|
||||
|
||||
/** List of valid S1G channels for Australia. */
|
||||
static const struct mmwlan_s1g_channel s1g_channels_AU[] = {
|
||||
/* Ctr Freq (Hz), Duty Cycle (%/100), Omit Control Response, Global Op Class, S1G Op Class, S1G Chan #, Op BW, Max Tx EIRP (dBm), Min Packet Spacing Window (microsec), airtime_min (microsec), airtime_max (microsec) */
|
||||
{ 915500000, 10000, false, 68, 22, 27, 1, 30, 0, 0, 0 },
|
||||
{ 916500000, 10000, false, 68, 22, 29, 1, 30, 0, 0, 0 },
|
||||
{ 917500000, 10000, false, 68, 22, 31, 1, 30, 0, 0, 0 },
|
||||
{ 918500000, 10000, false, 68, 22, 33, 1, 30, 0, 0, 0 },
|
||||
{ 919500000, 10000, false, 68, 22, 35, 1, 30, 0, 0, 0 },
|
||||
{ 920500000, 10000, false, 68, 22, 37, 1, 30, 0, 0, 0 },
|
||||
{ 921500000, 10000, false, 68, 22, 39, 1, 30, 0, 0, 0 },
|
||||
{ 922500000, 10000, false, 68, 22, 41, 1, 30, 0, 0, 0 },
|
||||
{ 923500000, 10000, false, 68, 22, 43, 1, 30, 0, 0, 0 },
|
||||
{ 924500000, 10000, false, 68, 22, 45, 1, 30, 0, 0, 0 },
|
||||
{ 925500000, 10000, false, 68, 22, 47, 1, 30, 0, 0, 0 },
|
||||
{ 926500000, 10000, false, 68, 22, 49, 1, 30, 0, 0, 0 },
|
||||
{ 927500000, 10000, false, 68, 22, 51, 1, 30, 0, 0, 0 },
|
||||
{ 917000000, 10000, false, 69, 23, 30, 2, 30, 0, 0, 0 },
|
||||
{ 919000000, 10000, false, 69, 23, 34, 2, 30, 0, 0, 0 },
|
||||
{ 921000000, 10000, false, 69, 23, 38, 2, 30, 0, 0, 0 },
|
||||
{ 923000000, 10000, false, 69, 23, 42, 2, 30, 0, 0, 0 },
|
||||
{ 925000000, 10000, false, 69, 23, 46, 2, 30, 0, 0, 0 },
|
||||
{ 927000000, 10000, false, 69, 23, 50, 2, 30, 0, 0, 0 },
|
||||
{ 918000000, 10000, false, 70, 24, 32, 4, 30, 0, 0, 0 },
|
||||
{ 922000000, 10000, false, 70, 24, 40, 4, 30, 0, 0, 0 },
|
||||
{ 926000000, 10000, false, 70, 24, 48, 4, 30, 0, 0, 0 },
|
||||
{ 924000000, 10000, false, 71, 25, 44, 8, 30, 0, 0, 0 },
|
||||
};
|
||||
|
||||
/** Channel list structure for Australia. */
|
||||
static const struct mmwlan_s1g_channel_list s1g_channel_list_AU = {
|
||||
.country_code = "AU",
|
||||
.num_channels = (sizeof(s1g_channels_AU)/sizeof(s1g_channels_AU[0])),
|
||||
.channels = s1g_channels_AU,
|
||||
};
|
||||
|
||||
/** List of valid S1G channels for EU. */
|
||||
static const struct mmwlan_s1g_channel s1g_channels_EU[] = {
|
||||
/* Ctr Freq (Hz), Duty Cycle (%/100), Omit Control Response, Global Op Class, S1G Op Class, S1G Chan #, Op BW, Max Tx EIRP (dBm), Min Packet Spacing Window (microsec), airtime_min (microsec), airtime_max (microsec) */
|
||||
{ 863500000, 280, false, 66, 6, 1, 1, 16, 0, 0, 0 },
|
||||
{ 864500000, 280, false, 66, 6, 3, 1, 16, 0, 0, 0 },
|
||||
{ 865500000, 280, false, 66, 6, 5, 1, 16, 0, 0, 0 },
|
||||
{ 866500000, 280, false, 66, 6, 7, 1, 16, 0, 0, 0 },
|
||||
{ 867500000, 280, false, 66, 6, 9, 1, 16, 0, 0, 0 },
|
||||
};
|
||||
|
||||
/** Channel list structure for EU. */
|
||||
static const struct mmwlan_s1g_channel_list s1g_channel_list_EU = {
|
||||
.country_code = "EU",
|
||||
.num_channels = (sizeof(s1g_channels_EU)/sizeof(s1g_channels_EU[0])),
|
||||
.channels = s1g_channels_EU,
|
||||
};
|
||||
|
||||
/** List of valid S1G channels for India. */
|
||||
static const struct mmwlan_s1g_channel s1g_channels_IN[] = {
|
||||
/* Ctr Freq (Hz), Duty Cycle (%/100), Omit Control Response, Global Op Class, S1G Op Class, S1G Chan #, Op BW, Max Tx EIRP (dBm), Min Packet Spacing Window (microsec), airtime_min (microsec), airtime_max (microsec) */
|
||||
{ 865500000, 280, false, 66, 6, 5, 1, 16, 0, 0, 0 },
|
||||
{ 866500000, 280, false, 66, 6, 7, 1, 16, 0, 0, 0 },
|
||||
{ 867500000, 280, false, 66, 6, 9, 1, 16, 0, 0, 0 },
|
||||
};
|
||||
|
||||
/** Channel list structure for India. */
|
||||
static const struct mmwlan_s1g_channel_list s1g_channel_list_IN = {
|
||||
.country_code = "IN",
|
||||
.num_channels = (sizeof(s1g_channels_IN)/sizeof(s1g_channels_IN[0])),
|
||||
.channels = s1g_channels_IN,
|
||||
};
|
||||
|
||||
/** List of valid S1G channels for Japan. */
|
||||
static const struct mmwlan_s1g_channel s1g_channels_JP[] = {
|
||||
/* Ctr Freq (Hz), Duty Cycle (%/100), Omit Control Response, Global Op Class, S1G Op Class, S1G Chan #, Op BW, Max Tx EIRP (dBm), Min Packet Spacing Window (microsec), airtime_min (microsec), airtime_max (microsec) */
|
||||
{ 921000000, 1000, true, 73, 8, 9, 1, 16, 2000, 2000, 100000 },
|
||||
{ 923000000, 1000, true, 73, 8, 13, 1, 16, 2000, 2000, 100000 },
|
||||
{ 924000000, 1000, true, 73, 8, 15, 1, 16, 2000, 2000, 100000 },
|
||||
{ 925000000, 1000, true, 73, 8, 17, 1, 16, 2000, 2000, 100000 },
|
||||
{ 926000000, 1000, true, 73, 8, 19, 1, 16, 2000, 2000, 100000 },
|
||||
{ 927000000, 1000, true, 73, 8, 21, 1, 16, 2000, 2000, 100000 },
|
||||
{ 923500000, 1000, true, 64, 9, 2, 2, 16, 2000, 2000, 100000 },
|
||||
{ 924500000, 1000, true, 64, 10, 4, 2, 16, 2000, 2000, 100000 },
|
||||
{ 925500000, 1000, true, 64, 9, 6, 2, 16, 2000, 2000, 100000 },
|
||||
{ 926500000, 1000, true, 64, 10, 8, 2, 16, 2000, 2000, 100000 },
|
||||
{ 924500000, 1000, true, 65, 11, 36, 4, 16, 2000, 2000, 100000 },
|
||||
{ 925500000, 1000, true, 65, 12, 38, 4, 16, 2000, 2000, 100000 },
|
||||
};
|
||||
|
||||
/** Channel list structure for Japan. */
|
||||
static const struct mmwlan_s1g_channel_list s1g_channel_list_JP = {
|
||||
.country_code = "JP",
|
||||
.num_channels = (sizeof(s1g_channels_JP)/sizeof(s1g_channels_JP[0])),
|
||||
.channels = s1g_channels_JP,
|
||||
};
|
||||
|
||||
/** List of valid S1G channels for South Korea. */
|
||||
static const struct mmwlan_s1g_channel s1g_channels_KR[] = {
|
||||
/* Ctr Freq (Hz), Duty Cycle (%/100), Omit Control Response, Global Op Class, S1G Op Class, S1G Chan #, Op BW, Max Tx EIRP (dBm), Min Packet Spacing Window (microsec), airtime_min (microsec), airtime_max (microsec) */
|
||||
{ 918000000, 10000, false, 74, 14, 1, 1, 4, 50000, 0, 4000000 },
|
||||
{ 919000000, 10000, false, 74, 14, 3, 1, 4, 50000, 0, 4000000 },
|
||||
{ 920000000, 10000, false, 74, 14, 5, 1, 4, 50000, 0, 4000000 },
|
||||
{ 921000000, 10000, false, 74, 14, 7, 1, 4, 50000, 0, 4000000 },
|
||||
{ 922000000, 10000, false, 74, 14, 9, 1, 10, 50000, 0, 4000000 },
|
||||
{ 923000000, 10000, false, 74, 14, 11, 1, 10, 50000, 0, 4000000 },
|
||||
{ 918500000, 10000, false, 75, 15, 2, 2, 4, 50000, 0, 4000000 },
|
||||
{ 920500000, 10000, false, 75, 15, 6, 2, 4, 50000, 0, 4000000 },
|
||||
{ 922500000, 10000, false, 75, 15, 10, 2, 10, 50000, 0, 4000000 },
|
||||
{ 921500000, 10000, false, 76, 16, 8, 4, 4, 50000, 0, 4000000 },
|
||||
{ 926500000, 10000, false, 74, 14, 18, 1, 17, 264, 0, 220000 }, /* Warning: regulatory requirements may not be met */
|
||||
{ 927500000, 10000, false, 74, 14, 20, 1, 17, 264, 0, 220000 }, /* Warning: regulatory requirements may not be met */
|
||||
{ 928500000, 10000, false, 74, 14, 22, 1, 17, 264, 0, 220000 }, /* Warning: regulatory requirements may not be met */
|
||||
{ 929500000, 10000, false, 74, 14, 24, 1, 17, 264, 0, 220000 }, /* Warning: regulatory requirements may not be met */
|
||||
{ 927000000, 10000, false, 75, 15, 19, 2, 20, 264, 0, 220000 }, /* Warning: regulatory requirements may not be met */
|
||||
{ 929000000, 10000, false, 75, 15, 23, 2, 20, 264, 0, 220000 }, /* Warning: regulatory requirements may not be met */
|
||||
};
|
||||
|
||||
/** Channel list structure for South Korea. */
|
||||
static const struct mmwlan_s1g_channel_list s1g_channel_list_KR = {
|
||||
.country_code = "KR",
|
||||
.num_channels = (sizeof(s1g_channels_KR)/sizeof(s1g_channels_KR[0])),
|
||||
.channels = s1g_channels_KR,
|
||||
};
|
||||
|
||||
/** List of valid S1G channels for New Zealand. */
|
||||
static const struct mmwlan_s1g_channel s1g_channels_NZ[] = {
|
||||
/* Ctr Freq (Hz), Duty Cycle (%/100), Omit Control Response, Global Op Class, S1G Op Class, S1G Chan #, Op BW, Max Tx EIRP (dBm), Min Packet Spacing Window (microsec), airtime_min (microsec), airtime_max (microsec) */
|
||||
{ 915500000, 10000, false, 68, 26, 27, 1, 30, 0, 0, 0 },
|
||||
{ 916500000, 10000, false, 68, 26, 29, 1, 30, 0, 0, 0 },
|
||||
{ 917500000, 10000, false, 68, 26, 31, 1, 30, 0, 0, 0 },
|
||||
{ 918500000, 10000, false, 68, 26, 33, 1, 30, 0, 0, 0 },
|
||||
{ 919500000, 10000, false, 68, 26, 35, 1, 30, 0, 0, 0 },
|
||||
{ 920500000, 10000, false, 68, 26, 37, 1, 36, 0, 0, 0 },
|
||||
{ 921500000, 10000, false, 68, 26, 39, 1, 36, 0, 0, 0 },
|
||||
{ 922500000, 10000, false, 68, 26, 41, 1, 36, 0, 0, 0 },
|
||||
{ 923500000, 10000, false, 68, 26, 43, 1, 36, 0, 0, 0 },
|
||||
{ 924500000, 10000, false, 68, 26, 45, 1, 36, 0, 0, 0 },
|
||||
{ 925500000, 10000, false, 68, 26, 47, 1, 36, 0, 0, 0 },
|
||||
{ 926500000, 10000, false, 68, 26, 49, 1, 36, 0, 0, 0 },
|
||||
{ 927500000, 10000, false, 68, 26, 51, 1, 36, 0, 0, 0 },
|
||||
{ 917000000, 10000, false, 69, 27, 30, 2, 30, 0, 0, 0 },
|
||||
{ 919000000, 10000, false, 69, 27, 34, 2, 30, 0, 0, 0 },
|
||||
{ 921000000, 10000, false, 69, 27, 38, 2, 36, 0, 0, 0 },
|
||||
{ 923000000, 10000, false, 69, 27, 42, 2, 36, 0, 0, 0 },
|
||||
{ 925000000, 10000, false, 69, 27, 46, 2, 36, 0, 0, 0 },
|
||||
{ 927000000, 10000, false, 69, 27, 50, 2, 36, 0, 0, 0 },
|
||||
{ 918000000, 10000, false, 70, 28, 32, 4, 30, 0, 0, 0 },
|
||||
{ 922000000, 10000, false, 70, 28, 40, 4, 36, 0, 0, 0 },
|
||||
{ 926000000, 10000, false, 70, 28, 48, 4, 36, 0, 0, 0 },
|
||||
{ 924000000, 10000, false, 71, 29, 44, 8, 36, 0, 0, 0 },
|
||||
};
|
||||
|
||||
/** Channel list structure for New Zealand. */
|
||||
static const struct mmwlan_s1g_channel_list s1g_channel_list_NZ = {
|
||||
.country_code = "NZ",
|
||||
.num_channels = (sizeof(s1g_channels_NZ)/sizeof(s1g_channels_NZ[0])),
|
||||
.channels = s1g_channels_NZ,
|
||||
};
|
||||
|
||||
/** List of valid S1G channels for Singapore. */
|
||||
static const struct mmwlan_s1g_channel s1g_channels_SG[] = {
|
||||
/* Ctr Freq (Hz), Duty Cycle (%/100), Omit Control Response, Global Op Class, S1G Op Class, S1G Chan #, Op BW, Max Tx EIRP (dBm), Min Packet Spacing Window (microsec), airtime_min (microsec), airtime_max (microsec) */
|
||||
{ 866500000, 277, false, 66, 17, 7, 1, 29, 100000, 0, 1000000 },
|
||||
{ 867500000, 277, false, 66, 17, 9, 1, 29, 100000, 0, 1000000 },
|
||||
{ 868500000, 277, false, 66, 17, 11, 1, 29, 100000, 0, 1000000 },
|
||||
{ 868000000, 277, false, 67, 19, 10, 2, 29, 100000, 0, 1000000 },
|
||||
{ 920500000, 10000, false, 68, 18, 37, 1, 22, 0, 0, 0 },
|
||||
{ 921500000, 10000, false, 68, 18, 39, 1, 22, 0, 0, 0 },
|
||||
{ 922500000, 10000, false, 68, 18, 41, 1, 22, 0, 0, 0 },
|
||||
{ 923500000, 10000, false, 68, 18, 43, 1, 22, 0, 0, 0 },
|
||||
{ 924500000, 10000, false, 68, 18, 45, 1, 22, 0, 0, 0 },
|
||||
{ 921000000, 10000, false, 69, 20, 38, 2, 22, 0, 0, 0 },
|
||||
{ 923000000, 10000, false, 69, 20, 42, 2, 22, 0, 0, 0 },
|
||||
{ 922000000, 10000, false, 70, 21, 40, 4, 22, 0, 0, 0 },
|
||||
};
|
||||
|
||||
/** Channel list structure for Singapore. */
|
||||
static const struct mmwlan_s1g_channel_list s1g_channel_list_SG = {
|
||||
.country_code = "SG",
|
||||
.num_channels = (sizeof(s1g_channels_SG)/sizeof(s1g_channels_SG[0])),
|
||||
.channels = s1g_channels_SG,
|
||||
};
|
||||
|
||||
/** List of valid S1G channels for USA. */
|
||||
static const struct mmwlan_s1g_channel s1g_channels_US[] = {
|
||||
/* Ctr Freq (Hz), Duty Cycle (%/100), Omit Control Response, Global Op Class, S1G Op Class, S1G Chan #, Op BW, Max Tx EIRP (dBm), Min Packet Spacing Window (microsec), airtime_min (microsec), airtime_max (microsec) */
|
||||
{ 902500000, 10000, false, 68, 1, 1, 1, 36, 0, 0, 0 }, /* Warning: regulatory requirements may not be met */
|
||||
{ 903500000, 10000, false, 68, 1, 3, 1, 36, 0, 0, 0 },
|
||||
{ 904500000, 10000, false, 68, 1, 5, 1, 36, 0, 0, 0 },
|
||||
{ 905500000, 10000, false, 68, 1, 7, 1, 36, 0, 0, 0 },
|
||||
{ 906500000, 10000, false, 68, 1, 9, 1, 36, 0, 0, 0 },
|
||||
{ 907500000, 10000, false, 68, 1, 11, 1, 36, 0, 0, 0 },
|
||||
{ 908500000, 10000, false, 68, 1, 13, 1, 36, 0, 0, 0 },
|
||||
{ 909500000, 10000, false, 68, 1, 15, 1, 36, 0, 0, 0 },
|
||||
{ 910500000, 10000, false, 68, 1, 17, 1, 36, 0, 0, 0 },
|
||||
{ 911500000, 10000, false, 68, 1, 19, 1, 36, 0, 0, 0 },
|
||||
{ 912500000, 10000, false, 68, 1, 21, 1, 36, 0, 0, 0 },
|
||||
{ 913500000, 10000, false, 68, 1, 23, 1, 36, 0, 0, 0 },
|
||||
{ 914500000, 10000, false, 68, 1, 25, 1, 36, 0, 0, 0 },
|
||||
{ 915500000, 10000, false, 68, 1, 27, 1, 36, 0, 0, 0 },
|
||||
{ 916500000, 10000, false, 68, 1, 29, 1, 36, 0, 0, 0 },
|
||||
{ 917500000, 10000, false, 68, 1, 31, 1, 36, 0, 0, 0 },
|
||||
{ 918500000, 10000, false, 68, 1, 33, 1, 36, 0, 0, 0 },
|
||||
{ 919500000, 10000, false, 68, 1, 35, 1, 36, 0, 0, 0 },
|
||||
{ 920500000, 10000, false, 68, 1, 37, 1, 36, 0, 0, 0 },
|
||||
{ 921500000, 10000, false, 68, 1, 39, 1, 36, 0, 0, 0 },
|
||||
{ 922500000, 10000, false, 68, 1, 41, 1, 36, 0, 0, 0 },
|
||||
{ 923500000, 10000, false, 68, 1, 43, 1, 36, 0, 0, 0 },
|
||||
{ 924500000, 10000, false, 68, 1, 45, 1, 36, 0, 0, 0 },
|
||||
{ 925500000, 10000, false, 68, 1, 47, 1, 36, 0, 0, 0 },
|
||||
{ 926500000, 10000, false, 68, 1, 49, 1, 36, 0, 0, 0 },
|
||||
{ 927500000, 10000, false, 68, 1, 51, 1, 36, 0, 0, 0 },
|
||||
{ 903000000, 10000, false, 69, 2, 2, 2, 36, 0, 0, 0 }, /* Warning: regulatory requirements may not be met */
|
||||
{ 905000000, 10000, false, 69, 2, 6, 2, 36, 0, 0, 0 },
|
||||
{ 907000000, 10000, false, 69, 2, 10, 2, 36, 0, 0, 0 },
|
||||
{ 909000000, 10000, false, 69, 2, 14, 2, 36, 0, 0, 0 },
|
||||
{ 911000000, 10000, false, 69, 2, 18, 2, 36, 0, 0, 0 },
|
||||
{ 913000000, 10000, false, 69, 2, 22, 2, 36, 0, 0, 0 },
|
||||
{ 915000000, 10000, false, 69, 2, 26, 2, 36, 0, 0, 0 },
|
||||
{ 917000000, 10000, false, 69, 2, 30, 2, 36, 0, 0, 0 },
|
||||
{ 919000000, 10000, false, 69, 2, 34, 2, 36, 0, 0, 0 },
|
||||
{ 921000000, 10000, false, 69, 2, 38, 2, 36, 0, 0, 0 },
|
||||
{ 923000000, 10000, false, 69, 2, 42, 2, 36, 0, 0, 0 },
|
||||
{ 925000000, 10000, false, 69, 2, 46, 2, 36, 0, 0, 0 },
|
||||
{ 927000000, 10000, false, 69, 2, 50, 2, 36, 0, 0, 0 },
|
||||
{ 906000000, 10000, false, 70, 3, 8, 4, 36, 0, 0, 0 },
|
||||
{ 910000000, 10000, false, 70, 3, 16, 4, 36, 0, 0, 0 },
|
||||
{ 914000000, 10000, false, 70, 3, 24, 4, 36, 0, 0, 0 },
|
||||
{ 918000000, 10000, false, 70, 3, 32, 4, 36, 0, 0, 0 },
|
||||
{ 922000000, 10000, false, 70, 3, 40, 4, 36, 0, 0, 0 },
|
||||
{ 926000000, 10000, false, 70, 3, 48, 4, 36, 0, 0, 0 },
|
||||
{ 908000000, 10000, false, 71, 4, 12, 8, 36, 0, 0, 0 },
|
||||
{ 916000000, 10000, false, 71, 4, 28, 8, 36, 0, 0, 0 },
|
||||
{ 924000000, 10000, false, 71, 4, 44, 8, 36, 0, 0, 0 },
|
||||
};
|
||||
|
||||
/** Channel list structure for USA. */
|
||||
static const struct mmwlan_s1g_channel_list s1g_channel_list_US = {
|
||||
.country_code = "US",
|
||||
.num_channels = (sizeof(s1g_channels_US)/sizeof(s1g_channels_US[0])),
|
||||
.channels = s1g_channels_US,
|
||||
};
|
||||
|
||||
/** Array of all channel list structs used for the regulatory database. */
|
||||
|
||||
static const struct mmwlan_s1g_channel_list *regulatory_db_domains[] = {
|
||||
&s1g_channel_list_AU,
|
||||
&s1g_channel_list_EU,
|
||||
&s1g_channel_list_IN,
|
||||
&s1g_channel_list_JP,
|
||||
&s1g_channel_list_KR,
|
||||
&s1g_channel_list_NZ,
|
||||
&s1g_channel_list_SG,
|
||||
&s1g_channel_list_US,
|
||||
};
|
||||
|
||||
/** Regulatory database. */
|
||||
static const struct mmwlan_regulatory_db regulatory_db = {
|
||||
.num_domains = (sizeof(regulatory_db_domains)/sizeof(regulatory_db_domains[0])),
|
||||
.domains = regulatory_db_domains,
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a pointer to regulatory_db. This function isn't strictly necessary, since regulatory_db
|
||||
* can be accessed directly, but will prevent the compiler from generated warnings about
|
||||
* regulatory_db being unused.
|
||||
*
|
||||
* @return Reference to the regulatory database
|
||||
*/
|
||||
static inline const struct mmwlan_regulatory_db *get_regulatory_db(void)
|
||||
{
|
||||
return ®ulatory_db;
|
||||
}
|
||||
|
||||
|
||||
/** \} */
|
||||
Binary file not shown.
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "MorseWlan",
|
||||
"version": "0.1.0",
|
||||
"description": "Morse Micro mm-iot-esp32 SDK vendored for Meshtastic HaLow transport. Static morselib (Apache-2.0) + open-source shims.",
|
||||
"frameworks": ["arduino", "espidf"],
|
||||
"platforms": ["espressif32"],
|
||||
"build": {
|
||||
"srcDir": "src",
|
||||
"srcFilter": ["+<*.c>"],
|
||||
"includeDir": "include",
|
||||
"libArchive": false,
|
||||
"extraScript": "extra_script.py"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright 2022-2024 Morse Micro
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/*
|
||||
* ----
|
||||
* Endianness operations
|
||||
* ----
|
||||
*/
|
||||
|
||||
#ifdef __big_endian__
|
||||
#define __BYTE_ORDER __BIG_ENDIAN
|
||||
#else
|
||||
#define __BYTE_ORDER __LITTLE_ENDIAN
|
||||
#endif
|
||||
|
||||
#define bswap_16(x) __builtin_bswap16(x)
|
||||
#define bswap_32(x) __builtin_bswap32(x)
|
||||
|
||||
#define INET_ADDRSTRLEN 16
|
||||
#define INET6_ADDRSTRLEN 46
|
||||
|
||||
/* Protocol families. */
|
||||
#define PF_INET 2 /* IP protocol family. */
|
||||
#define PF_INET6 10 /* IP version 6. */
|
||||
|
||||
/* Address families. */
|
||||
#define AF_INET PF_INET
|
||||
#define AF_INET6 PF_INET6
|
||||
|
||||
/*
|
||||
* ----
|
||||
* Type definitions
|
||||
* ----
|
||||
*/
|
||||
typedef signed char __s8;
|
||||
typedef unsigned char __u8;
|
||||
typedef signed short __s16;
|
||||
typedef unsigned short __u16;
|
||||
typedef signed int __s32;
|
||||
typedef unsigned int __u32;
|
||||
|
||||
struct in_addr {
|
||||
__u32 s_addr;
|
||||
};
|
||||
|
||||
struct in6_addr {
|
||||
union {
|
||||
__u32 u32_addr[4];
|
||||
__u8 u8_addr[16];
|
||||
} un;
|
||||
#define s6_addr un.u8_addr
|
||||
};
|
||||
|
||||
/* Stub function declaration for inet_ntop which is called from write_ipv4_info
|
||||
* function in robust_av.c. Since they are not used we will create a dummy
|
||||
* function declarion here.
|
||||
*/
|
||||
const char *inet_ntop(int __af, const void *__cp, char *__buf, int __len);
|
||||
|
||||
/* Rename crypto functions to match symbol name mangling in morselib for avoidance of namespace
|
||||
* collisions. */
|
||||
#define aes_decrypt mmint_aes_decrypt
|
||||
#define aes_decrypt_deinit mmint_aes_decrypt_deinit
|
||||
#define aes_decrypt_init mmint_aes_decrypt_init
|
||||
#define crypto_bignum_add mmint_crypto_bignum_add
|
||||
#define crypto_bignum_addmod mmint_crypto_bignum_addmod
|
||||
#define crypto_bignum_cmp mmint_crypto_bignum_cmp
|
||||
#define crypto_bignum_deinit mmint_crypto_bignum_deinit
|
||||
#define crypto_bignum_div mmint_crypto_bignum_div
|
||||
#define crypto_bignum_exptmod mmint_crypto_bignum_exptmod
|
||||
#define crypto_bignum_init mmint_crypto_bignum_init
|
||||
#define crypto_bignum_init_set mmint_crypto_bignum_init_set
|
||||
#define crypto_bignum_init_uint mmint_crypto_bignum_init_uint
|
||||
#define crypto_bignum_inverse mmint_crypto_bignum_inverse
|
||||
#define crypto_bignum_is_odd mmint_crypto_bignum_is_odd
|
||||
#define crypto_bignum_is_one mmint_crypto_bignum_is_one
|
||||
#define crypto_bignum_is_zero mmint_crypto_bignum_is_zero
|
||||
#define crypto_bignum_legendre mmint_crypto_bignum_legendre
|
||||
#define crypto_bignum_mod mmint_crypto_bignum_mod
|
||||
#define crypto_bignum_mulmod mmint_crypto_bignum_mulmod
|
||||
#define crypto_bignum_rand mmint_crypto_bignum_rand
|
||||
#define crypto_bignum_rshift mmint_crypto_bignum_rshift
|
||||
#define crypto_bignum_sqrmod mmint_crypto_bignum_sqrmod
|
||||
#define crypto_bignum_sub mmint_crypto_bignum_sub
|
||||
#define crypto_bignum_to_bin mmint_crypto_bignum_to_bin
|
||||
#define crypto_ec_deinit mmint_crypto_ec_deinit
|
||||
#define crypto_ec_get_a mmint_crypto_ec_get_a
|
||||
#define crypto_ec_get_b mmint_crypto_ec_get_b
|
||||
#define crypto_ec_get_generator mmint_crypto_ec_get_generator
|
||||
#define crypto_ec_get_order mmint_crypto_ec_get_order
|
||||
#define crypto_ec_get_prime mmint_crypto_ec_get_prime
|
||||
#define crypto_ec_init mmint_crypto_ec_init
|
||||
#define crypto_ec_order_len mmint_crypto_ec_order_len
|
||||
#define crypto_ec_point_add mmint_crypto_ec_point_add
|
||||
#define crypto_ec_point_cmp mmint_crypto_ec_point_cmp
|
||||
#define crypto_ec_point_x mmint_crypto_ec_point_x
|
||||
#define crypto_ec_point_compute_y_sqr mmint_crypto_ec_point_compute_y_sqr
|
||||
#define crypto_ec_point_deinit mmint_crypto_ec_point_deinit
|
||||
#define crypto_ec_point_from_bin mmint_crypto_ec_point_from_bin
|
||||
#define crypto_ec_point_init mmint_crypto_ec_point_init
|
||||
#define crypto_ec_point_invert mmint_crypto_ec_point_invert
|
||||
#define crypto_ec_point_is_at_infinity mmint_crypto_ec_point_is_at_infinity
|
||||
#define crypto_ec_point_is_on_curve mmint_crypto_ec_point_is_on_curve
|
||||
#define crypto_ec_point_mul mmint_crypto_ec_point_mul
|
||||
#define crypto_ec_point_to_bin mmint_crypto_ec_point_to_bin
|
||||
#define crypto_ec_prime_len mmint_crypto_ec_prime_len
|
||||
#define crypto_ec_prime_len_bits mmint_crypto_ec_prime_len_bits
|
||||
#define crypto_ecdh_deinit mmint_crypto_ecdh_deinit
|
||||
#define crypto_ecdh_get_pubkey mmint_crypto_ecdh_get_pubkey
|
||||
#define crypto_ecdh_init mmint_crypto_ecdh_init
|
||||
#define crypto_ecdh_init2 mmint_crypto_ecdh_init2
|
||||
#define crypto_ecdh_set_peerkey mmint_crypto_ecdh_set_peerkey
|
||||
#define crypto_ecdh_prime_len mmint_crypto_ecdh_prime_len
|
||||
#define crypto_get_random mmint_crypto_get_random
|
||||
#define crypto_unload mmint_crypto_unload
|
||||
#define hmac_md5 mmint_hmac_md5
|
||||
#define hmac_sha1 mmint_hmac_sha1
|
||||
#define hmac_sha1_vector mmint_hmac_sha1_vector
|
||||
#define hmac_sha256 mmint_hmac_sha256
|
||||
#define hmac_sha256_vector mmint_hmac_sha256_vector
|
||||
#define hmac_sha384 mmint_hmac_sha384
|
||||
#define hmac_sha384_vector mmint_hmac_sha384_vector
|
||||
#define hmac_sha512 mmint_hmac_sha512
|
||||
#define hmac_sha512_vector mmint_hmac_sha512_vector
|
||||
#define omac1_aes_vector mmint_omac1_aes_vector
|
||||
#define omac1_aes_128 mmint_omac1_aes_128
|
||||
#define pbkdf2_sha1 mmint_pbkdf2_sha1
|
||||
#define sha1_prf mmint_sha1_prf
|
||||
#define sha1_vector mmint_sha1_vector
|
||||
#define sha256_prf mmint_sha256_prf
|
||||
#define sha256_prf_bits mmint_sha256_prf_bits
|
||||
#define sha256_vector mmint_sha256_vector
|
||||
#define sha384_prf mmint_sha384_prf
|
||||
#define sha384_vector mmint_sha384_vector
|
||||
#define sha512_prf mmint_sha512_prf
|
||||
#define sha512_vector mmint_sha512_vector
|
||||
|
||||
#define wpabuf_alloc mmint_wpabuf_alloc
|
||||
#define wpabuf_alloc_copy mmint_wpabuf_alloc_copy
|
||||
#define wpabuf_clear_free mmint_wpabuf_clear_free
|
||||
#define wpabuf_put mmint_wpabuf_put
|
||||
@@ -0,0 +1,27 @@
|
||||
#ifdef USE_MM_IOT_ESP32
|
||||
/*
|
||||
* Stubs for LWIP netif callback API that Arduino-ESP32's prebuilt LWIP omits
|
||||
* (CONFIG_LWIP_NETIF_STATUS_CALLBACK / _LINK_CALLBACK are disabled in its
|
||||
* sdkconfig, so the real functions are absent from the static library).
|
||||
*
|
||||
* mmipal calls these once during init to register a single callback for
|
||||
* link-up / IP-configured events. With these stubs, the callback never fires —
|
||||
* mmwlan_register_link_state_cb (driven by the radio firmware) remains the
|
||||
* authoritative source of link state, so this only costs us LWIP-level
|
||||
* notifications (e.g. "DHCP got an address"). HaLowInterface polls
|
||||
* mmipal_get_ip_config when it needs to know.
|
||||
*/
|
||||
#include "lwip/netif.h"
|
||||
|
||||
void __attribute__((weak)) netif_set_link_callback(struct netif *netif, netif_status_callback_fn link_callback)
|
||||
{
|
||||
(void)netif;
|
||||
(void)link_callback;
|
||||
}
|
||||
|
||||
void __attribute__((weak)) netif_set_status_callback(struct netif *netif, netif_status_callback_fn status_callback)
|
||||
{
|
||||
(void)netif;
|
||||
(void)status_callback;
|
||||
}
|
||||
#endif
|
||||
Binary file not shown.
@@ -0,0 +1,216 @@
|
||||
#ifdef USE_MM_IOT_ESP32
|
||||
/*
|
||||
* Copyright 2024 Morse Micro
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
*/
|
||||
|
||||
#include "mmbuf.h"
|
||||
#include "mmosal.h"
|
||||
#include "mmutils.h"
|
||||
|
||||
static const struct mmbuf_ops mmbuf_heap_ops = {.free_mmbuf = mmosal_free};
|
||||
|
||||
struct mmbuf *mmbuf_alloc_on_heap(uint32_t space_at_start, uint32_t space_at_end)
|
||||
{
|
||||
struct mmbuf *mmbuf;
|
||||
uint8_t *buf;
|
||||
uint32_t alloc_len = MM_FAST_ROUND_UP(sizeof(*mmbuf), 4) + MM_FAST_ROUND_UP(space_at_start + space_at_end, 4);
|
||||
mmbuf = (struct mmbuf *)mmosal_malloc(alloc_len);
|
||||
if (mmbuf == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* We zero the buffer as a defensive measure to reduce the likelihood of unintentionally
|
||||
* leaking information. */
|
||||
memset((uint8_t *)mmbuf, 0, alloc_len);
|
||||
|
||||
buf = ((uint8_t *)mmbuf) + MM_FAST_ROUND_UP(sizeof(*mmbuf), 4);
|
||||
|
||||
mmbuf_init(mmbuf, buf, MM_FAST_ROUND_UP(space_at_start + space_at_end, 4), space_at_start, &mmbuf_heap_ops);
|
||||
|
||||
return mmbuf;
|
||||
}
|
||||
|
||||
struct mmbuf *mmbuf_make_copy_on_heap(struct mmbuf *original)
|
||||
{
|
||||
struct mmbuf *mmbuf;
|
||||
uint8_t *buf;
|
||||
uint32_t alloc_len = MM_FAST_ROUND_UP(sizeof(*original), 4) + original->buf_len;
|
||||
mmbuf = (struct mmbuf *)mmosal_malloc(alloc_len);
|
||||
if (mmbuf == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
buf = ((uint8_t *)mmbuf) + MM_FAST_ROUND_UP(sizeof(*mmbuf), 4);
|
||||
mmbuf_init(mmbuf, buf, original->buf_len, original->start_offset, &mmbuf_heap_ops);
|
||||
mmbuf->data_len = original->data_len;
|
||||
|
||||
if (original->data_len) {
|
||||
memcpy(mmbuf_get_data_start(mmbuf), mmbuf_get_data_start(original), mmbuf_get_data_length(original));
|
||||
}
|
||||
|
||||
return mmbuf;
|
||||
}
|
||||
|
||||
void mmbuf_release(struct mmbuf *mmbuf)
|
||||
{
|
||||
if (mmbuf == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
MMOSAL_ASSERT(mmbuf->ops != NULL && mmbuf->ops->free_mmbuf != NULL);
|
||||
mmbuf->ops->free_mmbuf(mmbuf);
|
||||
}
|
||||
|
||||
#ifdef MMBUF_SANITY
|
||||
static void mmbuf_list_sanity_check(struct mmbuf_list *list)
|
||||
{
|
||||
unsigned cnt = 0;
|
||||
struct mmbuf *walk;
|
||||
struct mmbuf *prev = NULL;
|
||||
|
||||
for (walk = list->head; walk != NULL; walk = walk->next) {
|
||||
cnt++;
|
||||
prev = walk;
|
||||
}
|
||||
|
||||
MMOSAL_ASSERT(cnt == list->len);
|
||||
MMOSAL_ASSERT(prev == list->tail);
|
||||
}
|
||||
#endif
|
||||
|
||||
void mmbuf_list_prepend(struct mmbuf_list *list, struct mmbuf *mmbuf)
|
||||
{
|
||||
mmbuf->next = list->head;
|
||||
list->head = mmbuf;
|
||||
list->len++;
|
||||
|
||||
if (list->tail == NULL) {
|
||||
list->tail = list->head;
|
||||
}
|
||||
|
||||
#ifdef MMBUF_SANITY
|
||||
mmbuf_list_sanity_check(list);
|
||||
#endif
|
||||
}
|
||||
|
||||
void mmbuf_list_append(struct mmbuf_list *list, struct mmbuf *mmbuf)
|
||||
{
|
||||
mmbuf->next = NULL;
|
||||
if (list->head == NULL) {
|
||||
list->head = mmbuf;
|
||||
list->tail = mmbuf;
|
||||
} else {
|
||||
list->tail->next = mmbuf;
|
||||
list->tail = mmbuf;
|
||||
}
|
||||
list->len++;
|
||||
|
||||
#ifdef MMBUF_SANITY
|
||||
mmbuf_list_sanity_check(list);
|
||||
#endif
|
||||
}
|
||||
|
||||
static struct mmbuf *mmbuf_find_prev(struct mmbuf_list *list, struct mmbuf *mmbuf)
|
||||
{
|
||||
struct mmbuf *walk, *next;
|
||||
for (walk = list->head, next = walk->next; next != NULL; walk = next, next = walk->next) {
|
||||
if (next == mmbuf) {
|
||||
return walk;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool mmbuf_list_remove(struct mmbuf_list *list, struct mmbuf *mmbuf)
|
||||
{
|
||||
struct mmbuf *prev = NULL;
|
||||
|
||||
if (list->head == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (list->head == mmbuf) {
|
||||
list->head = mmbuf->next;
|
||||
} else {
|
||||
prev = mmbuf_find_prev(list, mmbuf);
|
||||
if (prev == NULL) {
|
||||
return false;
|
||||
}
|
||||
prev->next = mmbuf->next;
|
||||
}
|
||||
|
||||
if (list->tail == mmbuf) {
|
||||
list->tail = prev;
|
||||
}
|
||||
|
||||
list->len--;
|
||||
mmbuf->next = NULL;
|
||||
|
||||
#ifdef MMBUF_SANITY
|
||||
mmbuf_list_sanity_check(list);
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
struct mmbuf *mmbuf_list_dequeue(struct mmbuf_list *list)
|
||||
{
|
||||
if (list->head == NULL) {
|
||||
return NULL;
|
||||
} else {
|
||||
struct mmbuf *mmbuf = list->head;
|
||||
list->head = mmbuf->next;
|
||||
list->len--;
|
||||
|
||||
if (list->tail == mmbuf) {
|
||||
list->tail = NULL;
|
||||
}
|
||||
|
||||
#ifdef MMBUF_SANITY
|
||||
mmbuf_list_sanity_check(list);
|
||||
#endif
|
||||
if (mmbuf != NULL) {
|
||||
mmbuf->next = NULL;
|
||||
}
|
||||
return mmbuf;
|
||||
}
|
||||
}
|
||||
|
||||
struct mmbuf *mmbuf_list_dequeue_tail(struct mmbuf_list *list)
|
||||
{
|
||||
if (list->tail == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct mmbuf *mmbuf = list->tail;
|
||||
mmbuf_list_remove(list, mmbuf);
|
||||
|
||||
return mmbuf;
|
||||
}
|
||||
|
||||
void mmbuf_list_clear(struct mmbuf_list *list)
|
||||
{
|
||||
struct mmbuf *walk;
|
||||
struct mmbuf *next;
|
||||
|
||||
#ifdef MMBUF_SANITY
|
||||
mmbuf_list_sanity_check(list);
|
||||
#endif
|
||||
|
||||
if (list->head == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (walk = list->head, next = walk->next; walk != NULL; walk = next, next = walk ? walk->next : NULL) {
|
||||
mmbuf_release(walk);
|
||||
}
|
||||
|
||||
list->len = 0;
|
||||
list->head = NULL;
|
||||
list->tail = NULL;
|
||||
}
|
||||
|
||||
#endif /* USE_MM_IOT_ESP32 */
|
||||
@@ -0,0 +1,457 @@
|
||||
/*
|
||||
* Copyright 2024 Morse Micro
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @defgroup MMBUF Morse Micro Buffer (mmbuf) API
|
||||
*
|
||||
* This API provides support for buffers tailored towards packet-like data that has
|
||||
* headers and trailers that are applied at subsequent layers.
|
||||
*
|
||||
* It is designed to support various backends for memory allocation. The default
|
||||
* is allocation on the heap, but other methods could be used due to the flexible API.
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "mmosal.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct mmbuf_ops;
|
||||
|
||||
/**
|
||||
* Core mmbuf data structure.
|
||||
*
|
||||
* @note The contents of this data structure should never need to be accessed directly. Rather
|
||||
* the various functions provided as part of this API should be used.
|
||||
*
|
||||
* @code
|
||||
* +----------------------------------------------------------+
|
||||
* | RESERVED | Data | RESERVED |
|
||||
* +----------------------------------------------------------+
|
||||
* ^ ^ ^ ^
|
||||
* | | | |
|
||||
* | |<-----------data_len--------->| |
|
||||
* | start_offset |
|
||||
* | |
|
||||
* |<-----------------------buf_len-------------------------->|
|
||||
* buf
|
||||
* @endcode
|
||||
*/
|
||||
struct mmbuf {
|
||||
/** The buffer where data is stored. */
|
||||
uint8_t *buf;
|
||||
/** Length of the buffer. */
|
||||
uint32_t buf_len;
|
||||
/** Offset where actual data starts in the buffer. */
|
||||
uint32_t start_offset;
|
||||
/** Length of actual data in the buffer. */
|
||||
uint32_t data_len;
|
||||
/** Reference to operations data structure for this mmbuf. */
|
||||
const struct mmbuf_ops *ops;
|
||||
/** Pointer that can be used to construct linked lists. */
|
||||
struct mmbuf *volatile next;
|
||||
};
|
||||
|
||||
/** Operations data structure for mmbuf. */
|
||||
struct mmbuf_ops {
|
||||
/** Free the given mmbuf. */
|
||||
void (*free_mmbuf)(void *mmbuf);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize an mmbuf header with the given values.
|
||||
*
|
||||
* @param mmbuf mmbuf to initialize.
|
||||
* @param buf Pointer to buffer.
|
||||
* @param buf_len Length of @p buf.
|
||||
* @param data_start_offset Initial value for @c start_offset.
|
||||
* @param ops Operations data structure.
|
||||
*/
|
||||
static inline void mmbuf_init(struct mmbuf *mmbuf, uint8_t *buf, uint32_t buf_len, uint32_t data_start_offset,
|
||||
const struct mmbuf_ops *ops)
|
||||
{
|
||||
memset(mmbuf, 0, sizeof(*mmbuf));
|
||||
mmbuf->buf = buf;
|
||||
mmbuf->buf_len = buf_len;
|
||||
mmbuf->start_offset = data_start_offset;
|
||||
mmbuf->ops = ops;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocate a new mmbuf on the heap (using @ref mmosal_malloc()).
|
||||
*
|
||||
* @param space_at_start Amount of space to reserve at start of buffer.
|
||||
* @param space_at_end Amount of space to reserve at end of buffer.
|
||||
*
|
||||
* @note @c start_offset will be set to @p space_at_start, and @c buf_len will be the sum
|
||||
* of @p space_at_start and @p space_at_end (rounded up to a multiple of 4).
|
||||
*
|
||||
* @returns newly allocated mmbuf on success or @c NULL on failure.
|
||||
*/
|
||||
struct mmbuf *mmbuf_alloc_on_heap(uint32_t space_at_start, uint32_t space_at_end);
|
||||
|
||||
/**
|
||||
* Make a copy of the given mmbuf. Note that regardless of the backend that allocated
|
||||
* @p original, the newly allocated mmbuf will be allocated on the heap using
|
||||
* @ref mmbuf_alloc_on_heap().
|
||||
*
|
||||
* @param original mmbuf to copy.
|
||||
*
|
||||
* @returns newly allocated mmbuf on success or @c NULL on failure.
|
||||
*/
|
||||
struct mmbuf *mmbuf_make_copy_on_heap(struct mmbuf *original);
|
||||
|
||||
/**
|
||||
* Release a reference to the given mmbuf. If this was the last reference (@c addition_ref_cnt
|
||||
* was 0) then the mmbuf will be freed using the appropriate op callback.
|
||||
*
|
||||
* @param mmbuf The mmbuf to release reference to. May be @c NULL.
|
||||
*/
|
||||
void mmbuf_release(struct mmbuf *mmbuf);
|
||||
|
||||
/**
|
||||
* Gets a pointer to the start of the data in the mmbuf.
|
||||
*
|
||||
* @param mmbuf The mmbuf to operate on.
|
||||
*
|
||||
* @returns a pointer to the start of the data in the mmbuf.
|
||||
*/
|
||||
static inline uint8_t *mmbuf_get_data_start(struct mmbuf *mmbuf)
|
||||
{
|
||||
return mmbuf->buf + mmbuf->start_offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a pointer to the end of the data in the mmbuf.
|
||||
*
|
||||
* @param mmbuf The mmbuf to operate on.
|
||||
*
|
||||
* @returns a pointer to the end of the data in the mmbuf.
|
||||
*/
|
||||
static inline uint8_t *mmbuf_get_data_end(struct mmbuf *mmbuf)
|
||||
{
|
||||
return mmbuf->buf + mmbuf->start_offset + mmbuf->data_len;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the length of the data currently in the mmbuf.
|
||||
*
|
||||
* @param mmbuf The mmbuf to operate on.
|
||||
*
|
||||
* @returns the length of the data currently in the mmbuf (note that this is different from the
|
||||
* length of the available buffer space).
|
||||
*/
|
||||
static inline uint32_t mmbuf_get_data_length(struct mmbuf *mmbuf)
|
||||
{
|
||||
return mmbuf->data_len;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount of space available for prepending to the data in the buffer.
|
||||
*
|
||||
* @param mmbuf The mmbuf to operate on.
|
||||
*
|
||||
* @returns the available space in bytes.
|
||||
*/
|
||||
static inline uint32_t mmbuf_available_space_at_start(struct mmbuf *mmbuf)
|
||||
{
|
||||
return mmbuf->start_offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount of space available for appending to the data in the buffer.
|
||||
*
|
||||
* @param mmbuf The mmbuf to operate on.
|
||||
*
|
||||
* @returns the available space in bytes.
|
||||
*/
|
||||
static inline uint32_t mmbuf_available_space_at_end(struct mmbuf *mmbuf)
|
||||
{
|
||||
return mmbuf->buf_len - (mmbuf->start_offset + mmbuf->data_len);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reserves space immediately before the data currently in the given mmbuf and returns
|
||||
* a pointer to this space.
|
||||
*
|
||||
* For a function that also copies data in, see @ref mmbuf_prepend_data().
|
||||
*
|
||||
* @warning @p len must be less than or equal to @ref mmbuf_available_space_at_start().
|
||||
*
|
||||
* @param mmbuf The mmbuf to operate on.
|
||||
* @param len Length of data to be prepended.
|
||||
*
|
||||
* @returns a pointer to the place in the buffer where the data should be put.
|
||||
*/
|
||||
static inline uint8_t *mmbuf_prepend(struct mmbuf *mmbuf, uint32_t len)
|
||||
{
|
||||
MMOSAL_ASSERT(len <= mmbuf_available_space_at_start(mmbuf));
|
||||
mmbuf->start_offset -= len;
|
||||
mmbuf->data_len += len;
|
||||
return mmbuf->buf + mmbuf->start_offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepends the given data to the data already in the mmbuf.
|
||||
*
|
||||
* @warning @p len must be less than or equal to @ref mmbuf_available_space_at_start().
|
||||
*
|
||||
* @warning The memory area pointed to by data must not overlap with the mmbuf data.
|
||||
*
|
||||
* @param mmbuf The mmbuf to operate on.
|
||||
* @param data The data to be prepended.
|
||||
* @param len Length of data to be prepended.
|
||||
*/
|
||||
static inline void mmbuf_prepend_data(struct mmbuf *mmbuf, const uint8_t *data, uint32_t len)
|
||||
{
|
||||
uint8_t *dest = mmbuf_prepend(mmbuf, len);
|
||||
memcpy(dest, data, len);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reserves space immediately after the data currently in the given mmbuf and returns
|
||||
* a pointer to this space.
|
||||
*
|
||||
* For a function that also copies data in, see @ref mmbuf_append_data().
|
||||
*
|
||||
* @warning @p len must be less than or equal to @ref mmbuf_available_space_at_end().
|
||||
*
|
||||
* @param mmbuf The mmbuf to operate on.
|
||||
* @param len Length of data to be append.
|
||||
*
|
||||
* @returns a pointer to the place in the buffer where the data should be put.
|
||||
*/
|
||||
static inline uint8_t *mmbuf_append(struct mmbuf *mmbuf, uint32_t len)
|
||||
{
|
||||
uint8_t *ret = mmbuf_get_data_end(mmbuf);
|
||||
MMOSAL_ASSERT(len <= mmbuf_available_space_at_end(mmbuf));
|
||||
mmbuf->data_len += len;
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the given data to the data already in the mmbuf.
|
||||
*
|
||||
* @warning @p len must be less than or equal to @ref mmbuf_available_space_at_start().
|
||||
*
|
||||
* @param mmbuf The mmbuf to operate on.
|
||||
* @param data The data to be prepended.
|
||||
* @param len Length of data to be prepended.
|
||||
*/
|
||||
static inline void mmbuf_append_data(struct mmbuf *mmbuf, const uint8_t *data, uint32_t len)
|
||||
{
|
||||
uint8_t *dest = mmbuf_append(mmbuf, len);
|
||||
memcpy(dest, data, len);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove data from the start of the mmbuf.
|
||||
*
|
||||
* @param mmbuf mmbuf to operate on.
|
||||
* @param len Length of data to remove.
|
||||
*
|
||||
* @returns a pointer to the removed data or NULL if the mmbuf data length was less than @p len.
|
||||
*/
|
||||
static inline uint8_t *mmbuf_remove_from_start(struct mmbuf *mmbuf, uint32_t len)
|
||||
{
|
||||
uint8_t *ret;
|
||||
|
||||
if (mmbuf_get_data_length(mmbuf) < len) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ret = mmbuf_get_data_start(mmbuf);
|
||||
|
||||
mmbuf->start_offset += len;
|
||||
mmbuf->data_len -= len;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove data from the end of the mmbuf.
|
||||
*
|
||||
* @param mmbuf mmbuf to operate on.
|
||||
* @param len Length of data to remove.
|
||||
*
|
||||
* @returns a pointer to the removed data or NULL if the mmbuf data length was less than @p len.
|
||||
*/
|
||||
static inline uint8_t *mmbuf_remove_from_end(struct mmbuf *mmbuf, uint32_t len)
|
||||
{
|
||||
uint8_t *ret;
|
||||
|
||||
if (mmbuf_get_data_length(mmbuf) < len) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ret = mmbuf_get_data_end(mmbuf) - len;
|
||||
|
||||
mmbuf->data_len -= len;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate the mmbuf data to the given length.
|
||||
*
|
||||
* @param mmbuf mmbuf to operate on.
|
||||
* @param len New data length. (Must be less than or equal to the data length
|
||||
* of the mmbuf).
|
||||
*/
|
||||
static inline void mmbuf_truncate(struct mmbuf *mmbuf, uint32_t len)
|
||||
{
|
||||
MMOSAL_ASSERT(len <= mmbuf->data_len);
|
||||
mmbuf->data_len = len;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------------------------- */
|
||||
|
||||
/** Structure that can be used as the head of a linked list of mmbufs that counts its length. */
|
||||
struct mmbuf_list {
|
||||
/** First mmbuf in the list. */
|
||||
struct mmbuf *volatile head;
|
||||
/** Last mmbuf in the list. */
|
||||
struct mmbuf *volatile tail;
|
||||
/** Length of the list. */
|
||||
volatile uint32_t len;
|
||||
};
|
||||
|
||||
/** Static initializer for @ref mmbuf_list. */
|
||||
#define MMBUF_LIST_INIT \
|
||||
{ \
|
||||
NULL, NULL, 0 \
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialization function for @ref mmbuf_list, for cases where @c MMBUF_LIST_INIT
|
||||
* cannot be used.
|
||||
*
|
||||
* @param list The mmbuf_list to init.
|
||||
*/
|
||||
static inline void mmbuf_list_init(struct mmbuf_list *list)
|
||||
{
|
||||
list->head = NULL;
|
||||
list->tail = NULL;
|
||||
list->len = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an mmbuf to the start of an mmbuf list.
|
||||
*
|
||||
* @param list The list to prepend to.
|
||||
* @param mmbuf The mmbuf to prepend.
|
||||
*/
|
||||
void mmbuf_list_prepend(struct mmbuf_list *list, struct mmbuf *mmbuf);
|
||||
|
||||
/**
|
||||
* Add an mmbuf to the end of an mmbuf list.
|
||||
*
|
||||
* @param list The list to append to.
|
||||
* @param mmbuf The mmbuf to append.
|
||||
*/
|
||||
void mmbuf_list_append(struct mmbuf_list *list, struct mmbuf *mmbuf);
|
||||
|
||||
/**
|
||||
* Remove an mmbuf from an mmbuf list.
|
||||
*
|
||||
* @param list The list to remove from.
|
||||
* @param mmbuf The mmbuf to remove.
|
||||
*
|
||||
* @returns @c true if the given @c mmbuf was present in @c list, else @c fase.
|
||||
*/
|
||||
bool mmbuf_list_remove(struct mmbuf_list *list, struct mmbuf *mmbuf);
|
||||
|
||||
/**
|
||||
* Remove the mmbuf at the head of the list and return it.
|
||||
*
|
||||
* @param list The list to dequeue from.
|
||||
*
|
||||
* @returns the dequeued mmbuf, or @c NULL if the list is empty.
|
||||
*/
|
||||
struct mmbuf *mmbuf_list_dequeue(struct mmbuf_list *list);
|
||||
|
||||
/**
|
||||
* Remove the mmbuf at the tail of the list and return it.
|
||||
*
|
||||
* @param list The list to dequeue from.
|
||||
*
|
||||
* @returns the dequeued mmbuf, or @c NULL if the list is empty.
|
||||
*/
|
||||
struct mmbuf *mmbuf_list_dequeue_tail(struct mmbuf_list *list);
|
||||
|
||||
/**
|
||||
* Remove all mmbufs from the list and return as a linked list.
|
||||
*
|
||||
* @param list The list to dequeue from.
|
||||
*
|
||||
* @returns the dequeued mmbufs, or @c NULL if the list is empty.
|
||||
*/
|
||||
static inline struct mmbuf *mmbuf_list_dequeue_all(struct mmbuf_list *list)
|
||||
{
|
||||
struct mmbuf *head = list->head;
|
||||
list->head = NULL;
|
||||
list->tail = NULL;
|
||||
list->len = 0;
|
||||
return head;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given mmbuf list is empty.
|
||||
*
|
||||
* @param list The list to check.
|
||||
*
|
||||
* @returns @c true if the list is empty, else @c false.
|
||||
*/
|
||||
static inline bool mmbuf_list_is_empty(struct mmbuf_list *list)
|
||||
{
|
||||
return (list->head == NULL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the head of the mmbuf list.
|
||||
*
|
||||
* @param list The list to peek into.
|
||||
*
|
||||
* @returns the mmbuf at the head of the list.
|
||||
*/
|
||||
static inline struct mmbuf *mmbuf_list_peek(struct mmbuf_list *list)
|
||||
{
|
||||
return list->head;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tail of the mmbuf list.
|
||||
*
|
||||
* @param list The list to peek into.
|
||||
*
|
||||
* @returns the mmbuf at the tail of the list.
|
||||
*/
|
||||
static inline struct mmbuf *mmbuf_list_peek_tail(struct mmbuf_list *list)
|
||||
{
|
||||
return list->tail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Free all the packets in the given list and reset the list to empty state.
|
||||
*
|
||||
* @param list The list to clear.
|
||||
*/
|
||||
void mmbuf_list_clear(struct mmbuf_list *list);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
/** @} */
|
||||
@@ -0,0 +1,50 @@
|
||||
#ifdef USE_MM_IOT_ESP32
|
||||
/*
|
||||
* Copyright 2024 Morse Micro
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include "mmcrc.h"
|
||||
|
||||
/**
|
||||
* Static table used for the table_driven implementation.
|
||||
*/
|
||||
static const uint16_t crc16_xmodem_lookup_table[256] = {
|
||||
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce,
|
||||
0xf1ef, 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c,
|
||||
0xf3ff, 0xe3de, 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee,
|
||||
0xf5cf, 0xc5ac, 0xd58d, 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, 0xb75b, 0xa77a, 0x9719, 0x8738,
|
||||
0xf7df, 0xe7fe, 0xd79d, 0xc7bc, 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, 0xc9cc, 0xd9ed, 0xe98e,
|
||||
0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, 0xdbfd, 0xcbdc,
|
||||
0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, 0xedae,
|
||||
0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
|
||||
0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e,
|
||||
0xe16f, 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c,
|
||||
0xe37f, 0xf35e, 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e,
|
||||
0xe54f, 0xd52c, 0xc50d, 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xa7db, 0xb7fa, 0x8799, 0x97b8,
|
||||
0xe75f, 0xf77e, 0xc71d, 0xd73c, 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, 0xd94c, 0xc96d, 0xf90e,
|
||||
0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, 0xcb7d, 0xdb5c,
|
||||
0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, 0xfd2e,
|
||||
0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
|
||||
0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1,
|
||||
0x1ef0};
|
||||
|
||||
/**
|
||||
* @note This implementation(with a few modifications) and corresponding table was generated using
|
||||
* pycrc v0.9.2 (MIT) using the XMODEM model. https://pycrc.org/. The code generated by pycrc
|
||||
* is not considered a substantial portion of the software, therefore the licence does not
|
||||
* cover the generated code, and the author of pycrc will not claim any copyright on the
|
||||
* generated code (https://pypi.org/project/pycrc/0.9.2/).
|
||||
*/
|
||||
uint16_t mmcrc_16_xmodem(uint16_t crc, const void *data, size_t data_len)
|
||||
{
|
||||
const uint8_t *d = (const uint8_t *)data;
|
||||
|
||||
while (data_len--) {
|
||||
crc = (crc16_xmodem_lookup_table[((crc >> 8) ^ *d++)] ^ (crc << 8));
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
#endif /* USE_MM_IOT_ESP32 */
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2024 Morse Micro
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @defgroup MMCRC Morse Micro Cyclic Redundancy Check (mmcrc) API
|
||||
*
|
||||
* This API provides support for CRC algorithms used by Morse Micro code.
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Compute the CRC-16 for the data buffer using the XMODEM model.
|
||||
*
|
||||
* @param crc Seed for CRC calc, zero in most cases this is zero (0). If chaining calls
|
||||
* then this is the output from the previous invocation.
|
||||
* @param data Pointer to the start of the data to calculate the crc over.
|
||||
* @param data_len Length of the data array in bytes.
|
||||
*
|
||||
* @return Returns the CRC value.
|
||||
*/
|
||||
uint16_t mmcrc_16_xmodem(uint16_t crc, const void *data, size_t data_len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
/** @} */
|
||||
@@ -0,0 +1,100 @@
|
||||
#ifdef USE_MM_IOT_ESP32
|
||||
/*
|
||||
* Copyright 2021-2023 Morse Micro
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include "mmhal.h"
|
||||
#include "mmosal.h"
|
||||
#include "mmutils.h"
|
||||
#include "mmwlan.h"
|
||||
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_random.h"
|
||||
#include "esp_system.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
void mmhal_init(void)
|
||||
{
|
||||
/* We initialise the MM_RESET_N Pin here so that we can hold the MM6108 in reset regardless of
|
||||
* whether the mmhal_wlan_init/deinit function have been called. This allows us to ensure the
|
||||
* chip is in its lowest power state. You may want to revise this depending on your particular
|
||||
* hardware configuration. */
|
||||
gpio_config_t io_conf = {};
|
||||
io_conf.intr_type = GPIO_INTR_DISABLE;
|
||||
io_conf.mode = GPIO_MODE_OUTPUT;
|
||||
io_conf.pin_bit_mask = (1 << CONFIG_MM_RESET_N);
|
||||
io_conf.pull_down_en = 0;
|
||||
io_conf.pull_up_en = 0;
|
||||
gpio_config(&io_conf);
|
||||
|
||||
gpio_set_level(CONFIG_MM_RESET_N, 0);
|
||||
|
||||
/* Initialise the gpio ISR handler service. This allows per-pin GPIO interrupt handlers and is
|
||||
* what is used to register all the wlan related interrupt. */
|
||||
gpio_install_isr_service(0);
|
||||
}
|
||||
|
||||
void mmhal_log_write(const uint8_t *data, size_t length)
|
||||
{
|
||||
while (length--) {
|
||||
putc(*data++, stdout);
|
||||
}
|
||||
}
|
||||
|
||||
void mmhal_log_flush(void) {}
|
||||
|
||||
void mmhal_read_mac_addr(uint8_t *mac_addr)
|
||||
{
|
||||
/* We do not override the MAC address here. Therefore the driver will attempt to read it from
|
||||
* the chip and failing that will assign a randomly generated address. */
|
||||
(void)(mac_addr);
|
||||
}
|
||||
|
||||
uint32_t mmhal_random_u32(uint32_t min, uint32_t max)
|
||||
{
|
||||
/* Note: the below implementation does not guarantee a uniform distribution. */
|
||||
|
||||
uint32_t random_value = esp_random();
|
||||
|
||||
if (min == 0 && max == UINT32_MAX) {
|
||||
return random_value;
|
||||
} else {
|
||||
/* Calculate the range and shift required to fit within [min, max] */
|
||||
return (random_value % (max - min + 1)) + min;
|
||||
}
|
||||
}
|
||||
|
||||
void mmhal_reset(void)
|
||||
{
|
||||
esp_restart();
|
||||
while (1) {
|
||||
}
|
||||
}
|
||||
|
||||
void mmhal_set_deep_sleep_veto(uint8_t veto_id)
|
||||
{
|
||||
MM_UNUSED(veto_id);
|
||||
}
|
||||
|
||||
void mmhal_clear_deep_sleep_veto(uint8_t veto_id)
|
||||
{
|
||||
MM_UNUSED(veto_id);
|
||||
}
|
||||
|
||||
void mmhal_set_led(uint8_t led, uint8_t level)
|
||||
{
|
||||
MM_UNUSED(led);
|
||||
MM_UNUSED(level);
|
||||
}
|
||||
|
||||
bool mmhal_get_hardware_version(char *version_buffer, size_t version_buffer_length)
|
||||
{
|
||||
/* Note: You need to identify the correct hardware and or version
|
||||
* here using whatever means available (GPIO's, version number stored in EEPROM, etc)
|
||||
* and return the correct string here. */
|
||||
return !mmosal_safer_strcpy(version_buffer, "MM-ESP32S3 V1.0", version_buffer_length);
|
||||
}
|
||||
|
||||
#endif /* USE_MM_IOT_ESP32 */
|
||||
@@ -0,0 +1,72 @@
|
||||
#ifdef USE_MM_IOT_ESP32
|
||||
/*
|
||||
* Copyright 2021-2023 Morse Micro
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include "mmhal_wlan.h"
|
||||
#include "mmosal.h"
|
||||
|
||||
/*
|
||||
* ---------------------------------------------------------------------------------------------
|
||||
* BCF Retrieval
|
||||
* ---------------------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/*
|
||||
* The following implementation reads the BCF File from the config store.
|
||||
*/
|
||||
|
||||
void mmhal_wlan_read_bcf_file(uint32_t offset, uint32_t requested_len, struct mmhal_robuf *robuf)
|
||||
{
|
||||
/** Points to the start of the BCF binary image. Defined as part of the Makefile */
|
||||
extern uint8_t bcf_binary_start;
|
||||
/** Points to the end of the BCF binary image. Defined as part of the Makefile */
|
||||
extern uint8_t bcf_binary_end;
|
||||
|
||||
size_t bcf_len = &bcf_binary_end - &bcf_binary_start;
|
||||
|
||||
/* Initialise robuf */
|
||||
robuf->buf = NULL;
|
||||
robuf->len = 0;
|
||||
robuf->free_arg = NULL;
|
||||
robuf->free_cb = NULL;
|
||||
|
||||
/* Sanity check */
|
||||
if (bcf_len < offset) {
|
||||
printf("Detected an attempt to start reading off the end of the bcf file.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
robuf->buf = (uint8_t *)&bcf_binary_start + offset;
|
||||
robuf->len = bcf_len - offset;
|
||||
robuf->len = (robuf->len < requested_len) ? robuf->len : requested_len;
|
||||
}
|
||||
|
||||
/*
|
||||
* ---------------------------------------------------------------------------------------------
|
||||
* Firmware Retrieval
|
||||
* ---------------------------------------------------------------------------------------------
|
||||
*/
|
||||
/** Points to the start of the firmware binary image. Defined as part of the Makefile */
|
||||
extern uint8_t firmware_binary_start;
|
||||
/** Points to the end of the firmware binary image. Defined as part of the Makefile */
|
||||
extern uint8_t firmware_binary_end;
|
||||
|
||||
void mmhal_wlan_read_fw_file(uint32_t offset, uint32_t requested_len, struct mmhal_robuf *robuf)
|
||||
{
|
||||
uint32_t firmware_len = &firmware_binary_end - &firmware_binary_start;
|
||||
if (offset > firmware_len) {
|
||||
printf("Detected an attempt to start read off the end of the firmware file.\n");
|
||||
robuf->buf = NULL;
|
||||
return;
|
||||
}
|
||||
|
||||
robuf->buf = (&firmware_binary_start + offset);
|
||||
firmware_len -= offset;
|
||||
|
||||
robuf->len = (firmware_len < requested_len) ? firmware_len : requested_len;
|
||||
}
|
||||
|
||||
#endif /* USE_MM_IOT_ESP32 */
|
||||
@@ -0,0 +1,726 @@
|
||||
#ifdef USE_MM_IOT_ESP32
|
||||
/*
|
||||
* Copyright 2021-2023 Morse Micro
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include "mmipal.h"
|
||||
#include "mmnetif.h"
|
||||
#include "mmosal.h"
|
||||
#include "mmutils.h"
|
||||
#include "mmwlan.h"
|
||||
|
||||
#include "lwip/api.h"
|
||||
#include "lwip/autoip.h"
|
||||
#include "lwip/def.h"
|
||||
#include "lwip/dhcp.h"
|
||||
#include "lwip/dhcp6.h"
|
||||
#include "lwip/dns.h"
|
||||
#include "lwip/etharp.h"
|
||||
#include "lwip/ethip6.h"
|
||||
#include "lwip/igmp.h"
|
||||
#include "lwip/inet.h"
|
||||
#include "lwip/ip4_frag.h"
|
||||
#include "lwip/ip6_frag.h"
|
||||
#include "lwip/ip_addr.h"
|
||||
#include "lwip/mem.h"
|
||||
#include "lwip/sockets.h"
|
||||
#include "lwip/stats.h"
|
||||
#include "lwip/tcp.h"
|
||||
#include "lwip/tcpip.h"
|
||||
#include "lwip/udp.h"
|
||||
|
||||
static struct mmipal_data {
|
||||
struct netif lwip_mmnetif;
|
||||
/** This stores the IPv4 link state for the IP stack. I.e., do we have an IP address or not. */
|
||||
enum mmipal_link_state ip_link_state;
|
||||
/** Flag requesting ARP response offload feature */
|
||||
bool offload_arp_response;
|
||||
/** ARP refresh offload interval in seconds */
|
||||
uint32_t offload_arp_refresh_s;
|
||||
bool dhcp_offload_init_complete;
|
||||
/** The link status callback function that has been registered. */
|
||||
mmipal_link_status_cb_fn_t link_status_callback;
|
||||
/** The extended link status callback function that has been registered. */
|
||||
mmipal_ext_link_status_cb_fn_t ext_link_status_callback;
|
||||
/** Argument for the extended link status callback function that has been registered. */
|
||||
void *ext_link_status_callback_arg;
|
||||
#if LWIP_IPV4
|
||||
enum mmipal_addr_mode ip4_mode;
|
||||
#endif
|
||||
#if LWIP_IPV6
|
||||
enum mmipal_ip6_addr_mode ip6_mode;
|
||||
#endif
|
||||
} mmipal_data = {};
|
||||
|
||||
/** Getter function to retrieve the global mmipal data structure.*/
|
||||
static inline struct mmipal_data *mmipal_get_data(void)
|
||||
{
|
||||
return &mmipal_data;
|
||||
}
|
||||
|
||||
static void netif_status_callback(struct netif *netif);
|
||||
|
||||
#if LWIP_IPV4
|
||||
|
||||
/**
|
||||
* DHCP Lease update callback, invoked when we get a new DHCP lease.
|
||||
*
|
||||
* @param lease_info The new DHCP lease.
|
||||
*/
|
||||
static void mmipal_dhcp_lease_updated(const struct mmwlan_dhcp_lease_info *lease_info, void *arg)
|
||||
{
|
||||
struct mmipal_data *data = mmipal_get_data();
|
||||
|
||||
ip4_addr_t ip_addr, netmask, gateway;
|
||||
ip_addr_t dns_addr = ip_addr_any;
|
||||
|
||||
MM_UNUSED(arg);
|
||||
|
||||
data->dhcp_offload_init_complete = true;
|
||||
|
||||
ip4_addr_set_u32(&ip_addr, lease_info->ip4_addr);
|
||||
ip4_addr_set_u32(&netmask, lease_info->mask4_addr);
|
||||
ip4_addr_set_u32(&gateway, lease_info->gw4_addr);
|
||||
ip4_addr_set_u32(ip_2_ip4(&dns_addr), lease_info->dns4_addr);
|
||||
|
||||
LOCK_TCPIP_CORE();
|
||||
netif_set_addr(&data->lwip_mmnetif, &ip_addr, &netmask, &gateway);
|
||||
dns_setserver(0, &dns_addr);
|
||||
UNLOCK_TCPIP_CORE();
|
||||
|
||||
netif_status_callback(&data->lwip_mmnetif);
|
||||
}
|
||||
|
||||
enum mmipal_status mmipal_get_ip_config(struct mmipal_ip_config *config)
|
||||
{
|
||||
struct mmipal_data *data = mmipal_get_data();
|
||||
char *result;
|
||||
|
||||
config->mode = data->ip4_mode;
|
||||
|
||||
result = ipaddr_ntoa_r(&data->lwip_mmnetif.ip_addr, config->ip_addr, sizeof(config->ip_addr));
|
||||
LWIP_ASSERT("IP buf too short", result != NULL);
|
||||
|
||||
result = ipaddr_ntoa_r(&data->lwip_mmnetif.netmask, config->netmask, sizeof(config->netmask));
|
||||
LWIP_ASSERT("IP buf too short", result != NULL);
|
||||
|
||||
result = ipaddr_ntoa_r(&data->lwip_mmnetif.gw, config->gateway_addr, sizeof(config->gateway_addr));
|
||||
LWIP_ASSERT("IP buf too short", result != NULL);
|
||||
|
||||
return MMIPAL_SUCCESS;
|
||||
}
|
||||
|
||||
enum mmipal_status mmipal_set_ip_config(const struct mmipal_ip_config *config)
|
||||
{
|
||||
struct mmipal_data *data = mmipal_get_data();
|
||||
int result;
|
||||
ip_addr_t ip_addr = ip_addr_any;
|
||||
ip_addr_t netmask = ip_addr_any;
|
||||
ip_addr_t gateway = ip_addr_any;
|
||||
struct netif *netif = &data->lwip_mmnetif;
|
||||
|
||||
if (config->mode != MMIPAL_DHCP_OFFLOAD && data->ip4_mode == MMIPAL_DHCP_OFFLOAD) {
|
||||
printf("Once enabled DHCP offload mode cannot be disabled\n");
|
||||
return MMIPAL_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
switch (config->mode) {
|
||||
case MMIPAL_DISABLED:
|
||||
printf("%s mode not supported\n", "DISABLED");
|
||||
return MMIPAL_INVALID_ARGUMENT;
|
||||
|
||||
case MMIPAL_AUTOIP:
|
||||
printf("%s mode not supported\n", "AutoIP");
|
||||
return MMIPAL_INVALID_ARGUMENT;
|
||||
|
||||
case MMIPAL_DHCP_OFFLOAD:
|
||||
/* Currently we only support enabling DHCP offload when initialising */
|
||||
printf("%s mode not supported\n", "DHCP_OFFLOAD");
|
||||
return MMIPAL_INVALID_ARGUMENT;
|
||||
|
||||
case MMIPAL_STATIC:
|
||||
result = ipaddr_aton(config->ip_addr, &ip_addr);
|
||||
if (!result) {
|
||||
return MMIPAL_INVALID_ARGUMENT;
|
||||
}
|
||||
result = ipaddr_aton(config->netmask, &netmask);
|
||||
if (!result) {
|
||||
return MMIPAL_INVALID_ARGUMENT;
|
||||
}
|
||||
result = ipaddr_aton(config->gateway_addr, &gateway);
|
||||
if (!result) {
|
||||
return MMIPAL_INVALID_ARGUMENT;
|
||||
}
|
||||
break;
|
||||
|
||||
case MMIPAL_DHCP:
|
||||
break;
|
||||
}
|
||||
|
||||
LOCK_TCPIP_CORE();
|
||||
|
||||
if (config->mode != MMIPAL_DHCP && data->ip4_mode == MMIPAL_DHCP) {
|
||||
/* Stop DHCP if it was started earlier before setting static IP */
|
||||
dhcp_stop(netif);
|
||||
}
|
||||
|
||||
data->ip4_mode = config->mode;
|
||||
|
||||
netif_set_addr(netif, ip_2_ip4(&ip_addr), ip_2_ip4(&netmask), ip_2_ip4(&gateway));
|
||||
if (data->ip4_mode == MMIPAL_DHCP) {
|
||||
result = dhcp_start(netif);
|
||||
LWIP_ASSERT("DHCP start error", result == ERR_OK);
|
||||
}
|
||||
|
||||
UNLOCK_TCPIP_CORE();
|
||||
return MMIPAL_SUCCESS;
|
||||
}
|
||||
|
||||
enum mmipal_status mmipal_get_ip_broadcast_addr(mmipal_ip_addr_t broadcast_addr)
|
||||
{
|
||||
struct mmipal_data *data = mmipal_get_data();
|
||||
char *result;
|
||||
|
||||
uint32_t ip_addr = ip_addr_get_ip4_u32(&data->lwip_mmnetif.ip_addr);
|
||||
uint32_t netmask = ip_addr_get_ip4_u32(&data->lwip_mmnetif.netmask);
|
||||
uint32_t broadcast_u32 = (ip_addr & netmask) | (0xffffffff & ~netmask);
|
||||
ip_addr_t broadcast_ip_addr;
|
||||
ip_addr_t *_broadcast_ip_addr = &broadcast_ip_addr;
|
||||
ip_addr_set_ip4_u32(_broadcast_ip_addr, broadcast_u32);
|
||||
|
||||
result = ipaddr_ntoa_r(&broadcast_ip_addr, broadcast_addr, MMIPAL_IPADDR_STR_MAXLEN);
|
||||
LWIP_ASSERT("IP buf too short", result != NULL);
|
||||
|
||||
return MMIPAL_SUCCESS;
|
||||
}
|
||||
|
||||
#else
|
||||
enum mmipal_status mmipal_get_ip_config(struct mmipal_ip_config *config)
|
||||
{
|
||||
MM_UNUSED(config);
|
||||
LWIP_ASSERT("IPv4 not enabled", false);
|
||||
return MMIPAL_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
enum mmipal_status mmipal_set_ip_config(const struct mmipal_ip_config *config)
|
||||
{
|
||||
MM_UNUSED(config);
|
||||
LWIP_ASSERT("IPv4 not enabled", false);
|
||||
return MMIPAL_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
enum mmipal_status mmipal_get_ip_broadcast_addr(mmipal_ip_addr_t broadcast_addr)
|
||||
{
|
||||
MM_UNUSED(broadcast_addr);
|
||||
LWIP_ASSERT("IPv4 not enabled", false);
|
||||
return MMIPAL_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if LWIP_IPV6
|
||||
|
||||
enum mmipal_status mmipal_get_ip6_config(struct mmipal_ip6_config *config)
|
||||
{
|
||||
struct mmipal_data *data = mmipal_get_data();
|
||||
unsigned ii;
|
||||
struct netif *netif = &data->lwip_mmnetif;
|
||||
|
||||
if (config == NULL) {
|
||||
return MMIPAL_INVALID_ARGUMENT;
|
||||
}
|
||||
config->ip6_mode = data->ip6_mode;
|
||||
for (ii = 0; ii < LWIP_IPV6_NUM_ADDRESSES; ii++) {
|
||||
char *result;
|
||||
const ip_addr_t *addr = &ip6_addr_any;
|
||||
|
||||
if (ip6_addr_isvalid(netif_ip6_addr_state(netif, ii))) {
|
||||
addr = &data->lwip_mmnetif.ip6_addr[ii];
|
||||
}
|
||||
|
||||
result = ipaddr_ntoa_r(addr, config->ip6_addr[ii], sizeof(config->ip6_addr[ii]));
|
||||
LWIP_ASSERT("IP buf too short", result != NULL);
|
||||
}
|
||||
return MMIPAL_SUCCESS;
|
||||
}
|
||||
|
||||
enum mmipal_status mmipal_set_ip6_config(const struct mmipal_ip6_config *config)
|
||||
{
|
||||
struct mmipal_data *data = mmipal_get_data();
|
||||
struct netif *netif = &data->lwip_mmnetif;
|
||||
err_t result;
|
||||
unsigned ii;
|
||||
ip_addr_t ip6_addr[LWIP_IPV6_NUM_ADDRESSES];
|
||||
|
||||
for (ii = 0; ii < LWIP_IPV6_NUM_ADDRESSES; ii++) {
|
||||
int result = ipaddr_aton(config->ip6_addr[ii], &ip6_addr[ii]);
|
||||
if (!result) {
|
||||
return MMIPAL_INVALID_ARGUMENT;
|
||||
}
|
||||
}
|
||||
|
||||
LOCK_TCPIP_CORE();
|
||||
|
||||
if (config->ip6_mode == MMIPAL_IP6_STATIC) {
|
||||
if (data->ip6_mode != MMIPAL_IP6_STATIC) {
|
||||
#if LWIP_IPV6_DHCP6
|
||||
dhcp6_disable(netif);
|
||||
#endif
|
||||
netif_set_ip6_autoconfig_enabled(netif, 0);
|
||||
data->ip6_mode = MMIPAL_IP6_STATIC;
|
||||
}
|
||||
|
||||
if (!ip6_addr_islinklocal(ip_2_ip6(&(ip6_addr[0])))) {
|
||||
printf("First address must be linklocal address (address start with fe80)\n");
|
||||
}
|
||||
|
||||
for (ii = 0; ii < LWIP_IPV6_NUM_ADDRESSES; ii++) {
|
||||
if (ip_addr_isany_val(ip6_addr[ii])) {
|
||||
netif_ip6_addr_set(netif, ii, IP6_ADDR_ANY6);
|
||||
netif_ip6_addr_set_state(netif, ii, IP6_ADDR_INVALID);
|
||||
} else {
|
||||
netif_ip6_addr_set(netif, ii, ip_2_ip6(&(ip6_addr[ii])));
|
||||
netif_ip6_addr_set_state(netif, ii, IP6_ADDR_TENTATIVE);
|
||||
netif_ip6_addr_set_valid_life(netif, ii, IP6_ADDR_LIFE_STATIC);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (data->ip6_mode == MMIPAL_IP6_STATIC) {
|
||||
for (ii = 0; ii < LWIP_IPV6_NUM_ADDRESSES; ii++) {
|
||||
netif_ip6_addr_set(netif, ii, IP6_ADDR_ANY6);
|
||||
netif_ip6_addr_set_state(netif, ii, IP6_ADDR_INVALID);
|
||||
}
|
||||
}
|
||||
netif_set_ip6_autoconfig_enabled(netif, 1);
|
||||
netif_create_ip6_linklocal_address(netif, 1);
|
||||
data->ip6_mode = MMIPAL_IP6_AUTOCONFIG;
|
||||
}
|
||||
|
||||
if (config->ip6_mode == MMIPAL_IP6_DHCP6_STATELESS)
|
||||
#if LWIP_IPV6_DHCP6
|
||||
{
|
||||
result = dhcp6_enable_stateless(netif);
|
||||
LWIP_ASSERT("Stateless DHCP6 start error", result == ERR_OK);
|
||||
data->ip6_mode = MMIPAL_IP6_DHCP6_STATELESS;
|
||||
} else {
|
||||
dhcp6_disable(netif);
|
||||
}
|
||||
#else
|
||||
{
|
||||
printf("LWIP_IPV6_DHCP6 is not enabled\n");
|
||||
}
|
||||
#endif
|
||||
|
||||
UNLOCK_TCPIP_CORE();
|
||||
return MMIPAL_SUCCESS;
|
||||
}
|
||||
|
||||
#else
|
||||
enum mmipal_status mmipal_get_ip6_config(struct mmipal_ip6_config *config)
|
||||
{
|
||||
MM_UNUSED(config);
|
||||
LWIP_ASSERT("IPv6 not enabled", false);
|
||||
return MMIPAL_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
enum mmipal_status mmipal_set_ip6_config(const struct mmipal_ip6_config *config)
|
||||
{
|
||||
MM_UNUSED(config);
|
||||
LWIP_ASSERT("IPv6 not enabled", false);
|
||||
return MMIPAL_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
static bool mmipal_link_status_check(struct netif *netif)
|
||||
{
|
||||
bool ip4_addr_check = true;
|
||||
|
||||
#if LWIP_IPV4
|
||||
ip4_addr_check = !ip_addr_isany(&(netif->ip_addr));
|
||||
#endif
|
||||
|
||||
return ip4_addr_check && netif_is_link_up(netif);
|
||||
}
|
||||
|
||||
/** Handler for @c netif status callbacks from LWIP. */
|
||||
static void netif_status_callback(struct netif *netif)
|
||||
{
|
||||
struct mmipal_data *data = mmipal_get_data();
|
||||
enum mmipal_link_state new_link_state = MMIPAL_LINK_DOWN;
|
||||
|
||||
#if LWIP_IPV4
|
||||
if (data->ip4_mode == MMIPAL_DHCP_OFFLOAD) {
|
||||
/* Initialize DHCP offload on link up */
|
||||
if (mmwlan_enable_dhcp_offload(mmipal_dhcp_lease_updated, NULL) != MMWLAN_SUCCESS) {
|
||||
printf("Failed to enable DHCP offload!\n");
|
||||
}
|
||||
|
||||
if (!data->dhcp_offload_init_complete) {
|
||||
/* This just prevents a spurious 'Link Up' message on very first call */
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (mmipal_link_status_check(netif)) {
|
||||
new_link_state = MMIPAL_LINK_UP;
|
||||
}
|
||||
|
||||
if (data->ip_link_state != new_link_state) {
|
||||
data->ip_link_state = new_link_state;
|
||||
|
||||
if (data->link_status_callback || data->ext_link_status_callback) {
|
||||
struct mmipal_link_status link_status;
|
||||
memset(&link_status, 0, sizeof(link_status));
|
||||
link_status.link_state = data->ip_link_state;
|
||||
|
||||
#if LWIP_IPV4
|
||||
char *result = ipaddr_ntoa_r(&netif->ip_addr, link_status.ip_addr, sizeof(link_status.ip_addr));
|
||||
LWIP_ASSERT("IP buf too short", result != NULL);
|
||||
|
||||
result = ipaddr_ntoa_r(&netif->netmask, link_status.netmask, sizeof(link_status.netmask));
|
||||
LWIP_ASSERT("IP buf too short", result != NULL);
|
||||
|
||||
result = ipaddr_ntoa_r(&netif->gw, link_status.gateway, sizeof(link_status.gateway));
|
||||
LWIP_ASSERT("IP buf too short", result != NULL);
|
||||
|
||||
if (data->ip_link_state == MMIPAL_LINK_UP) {
|
||||
/* Check if ARP response offload feature is enabled */
|
||||
if (data->offload_arp_response) {
|
||||
mmwlan_enable_arp_response_offload(ip4_addr_get_u32(netif_ip4_addr(netif)));
|
||||
}
|
||||
|
||||
/* Check if ARP refresh offload feature is enabled */
|
||||
if (data->offload_arp_refresh_s > 0) {
|
||||
mmwlan_enable_arp_refresh_offload(data->offload_arp_refresh_s, ip4_addr_get_u32(netif_ip4_gw(netif)), true);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (data->link_status_callback) {
|
||||
data->link_status_callback(&link_status);
|
||||
}
|
||||
if (data->ext_link_status_callback) {
|
||||
data->ext_link_status_callback(&link_status, data->ext_link_status_callback_arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void mmipal_set_link_status_callback(mmipal_link_status_cb_fn_t fn)
|
||||
{
|
||||
struct mmipal_data *data = mmipal_get_data();
|
||||
data->link_status_callback = fn;
|
||||
}
|
||||
|
||||
void mmipal_set_ext_link_status_callback(mmipal_ext_link_status_cb_fn_t fn, void *arg)
|
||||
{
|
||||
struct mmipal_data *data = mmipal_get_data();
|
||||
data->ext_link_status_callback = fn;
|
||||
data->ext_link_status_callback_arg = arg;
|
||||
}
|
||||
|
||||
static volatile bool tcpip_init_done = false;
|
||||
|
||||
struct lwip_init_args {
|
||||
enum mmipal_addr_mode mode;
|
||||
enum mmipal_ip6_addr_mode ip6_mode;
|
||||
ip_addr_t ip_addr;
|
||||
ip_addr_t netmask;
|
||||
ip_addr_t gateway_addr;
|
||||
ip_addr_t ip6_addr;
|
||||
};
|
||||
|
||||
static void tcpip_init_done_handler(void *arg)
|
||||
{
|
||||
struct mmipal_data *data = mmipal_get_data();
|
||||
struct netif *netif = &data->lwip_mmnetif;
|
||||
struct lwip_init_args *args = (struct lwip_init_args *)arg;
|
||||
|
||||
netif_add_noaddr(netif, NULL, mmnetif_init, tcpip_input);
|
||||
netif_set_default(netif);
|
||||
netif_set_up(netif);
|
||||
|
||||
#if LWIP_IPV4
|
||||
err_t result;
|
||||
data->ip4_mode = args->mode;
|
||||
if (args->mode == MMIPAL_DHCP) {
|
||||
result = dhcp_start(netif);
|
||||
LWIP_ASSERT("DHCP start error", result == ERR_OK);
|
||||
} else if (args->mode == MMIPAL_STATIC) {
|
||||
netif_set_addr(netif, ip_2_ip4(&(args->ip_addr)), ip_2_ip4(&(args->netmask)), ip_2_ip4(&(args->gateway_addr)));
|
||||
}
|
||||
#endif
|
||||
|
||||
netif_set_link_callback(netif, netif_status_callback);
|
||||
netif_set_status_callback(netif, netif_status_callback);
|
||||
|
||||
#if LWIP_IPV6
|
||||
err_t result6;
|
||||
data->ip6_mode = args->ip6_mode;
|
||||
if (args->ip6_mode == MMIPAL_IP6_STATIC) {
|
||||
netif_ip6_addr_set(netif, 0, ip_2_ip6(&(args->ip6_addr)));
|
||||
netif_ip6_addr_set_state(netif, 0, IP6_ADDR_TENTATIVE);
|
||||
} else if (data->ip6_mode == MMIPAL_IP6_AUTOCONFIG) {
|
||||
netif_set_ip6_autoconfig_enabled(netif, 1);
|
||||
netif_create_ip6_linklocal_address(netif, 1);
|
||||
} else if (data->ip6_mode == MMIPAL_IP6_DHCP6_STATELESS)
|
||||
#if LWIP_IPV6_DHCP6
|
||||
{
|
||||
result6 = dhcp6_enable_stateless(netif);
|
||||
LWIP_ASSERT("Stateless DHCP6 start error", result6 == ERR_OK);
|
||||
}
|
||||
#else
|
||||
{
|
||||
printf("LWIP_IPV6_DHCP6 is not enabled\n");
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
mmosal_free(args);
|
||||
|
||||
tcpip_init_done = true;
|
||||
}
|
||||
|
||||
enum mmipal_status mmipal_init(const struct mmipal_init_args *args)
|
||||
{
|
||||
struct mmipal_data *data = mmipal_get_data();
|
||||
enum mmipal_status status = MMIPAL_INVALID_ARGUMENT;
|
||||
int result;
|
||||
|
||||
struct lwip_init_args *lwip_args = (struct lwip_init_args *)mmosal_malloc(sizeof(*lwip_args));
|
||||
if (lwip_args == NULL) {
|
||||
printf("malloc failure\n");
|
||||
return MMIPAL_NO_MEM;
|
||||
}
|
||||
|
||||
memset(lwip_args, 0, sizeof(*lwip_args));
|
||||
lwip_args->mode = args->mode;
|
||||
lwip_args->ip6_mode = args->ip6_mode;
|
||||
|
||||
data->link_status_callback = NULL;
|
||||
|
||||
data->offload_arp_response = args->offload_arp_response;
|
||||
data->offload_arp_refresh_s = args->offload_arp_refresh_s;
|
||||
|
||||
/* Validate arguments */
|
||||
#if LWIP_IPV4
|
||||
switch (args->mode) {
|
||||
case MMIPAL_DISABLED:
|
||||
printf("%s mode not supported\n", "DISABLED");
|
||||
goto exit;
|
||||
|
||||
case MMIPAL_DHCP_OFFLOAD:
|
||||
case MMIPAL_STATIC:
|
||||
result = ipaddr_aton(args->ip_addr, &lwip_args->ip_addr);
|
||||
if (!result) {
|
||||
goto exit;
|
||||
}
|
||||
result = ipaddr_aton(args->netmask, &lwip_args->netmask);
|
||||
if (!result) {
|
||||
goto exit;
|
||||
}
|
||||
result = ipaddr_aton(args->gateway_addr, &lwip_args->gateway_addr);
|
||||
if (!result) {
|
||||
goto exit;
|
||||
}
|
||||
|
||||
if (ip_addr_isany_val(lwip_args->ip_addr)) {
|
||||
printf("IP address not specified\n");
|
||||
goto exit;
|
||||
}
|
||||
break;
|
||||
|
||||
case MMIPAL_DHCP:
|
||||
if (LWIP_DHCP == 0) {
|
||||
printf("DHCP not compiled in\n");
|
||||
goto exit;
|
||||
}
|
||||
break;
|
||||
|
||||
case MMIPAL_AUTOIP:
|
||||
printf("%s mode not supported\n", "AutoIP");
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if LWIP_IPV6
|
||||
switch (args->ip6_mode) {
|
||||
case MMIPAL_IP6_DISABLED:
|
||||
break;
|
||||
|
||||
case MMIPAL_IP6_STATIC:
|
||||
result = ipaddr_aton(args->ip6_addr, &lwip_args->ip6_addr);
|
||||
if (!result) {
|
||||
goto exit;
|
||||
}
|
||||
|
||||
if (ip_addr_isany_val(lwip_args->ip6_addr)) {
|
||||
printf("IP address not specified\n");
|
||||
goto exit;
|
||||
}
|
||||
break;
|
||||
|
||||
case MMIPAL_IP6_AUTOCONFIG:
|
||||
if (LWIP_IPV6_AUTOCONFIG == 0) {
|
||||
printf("AUTOCONFIG not compiled in\n");
|
||||
goto exit;
|
||||
}
|
||||
break;
|
||||
|
||||
case MMIPAL_IP6_DHCP6_STATELESS:
|
||||
if (LWIP_IPV6_DHCP6_STATELESS == 0) {
|
||||
printf("DHCP6_STATELESS not compiled in\n");
|
||||
goto exit;
|
||||
}
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
|
||||
tcpip_init(tcpip_init_done_handler, lwip_args);
|
||||
|
||||
/* Block until initialisation is complete */
|
||||
while (!tcpip_init_done) {
|
||||
mmosal_task_sleep(10);
|
||||
}
|
||||
|
||||
return MMIPAL_SUCCESS;
|
||||
|
||||
exit:
|
||||
mmosal_free(lwip_args);
|
||||
return status;
|
||||
}
|
||||
|
||||
void mmipal_get_link_packet_counts(uint32_t *tx_packets, uint32_t *rx_packets)
|
||||
{
|
||||
#if LWIP_STATS
|
||||
*tx_packets = lwip_stats.link.xmit;
|
||||
*rx_packets = lwip_stats.link.recv;
|
||||
#else
|
||||
*tx_packets = 0;
|
||||
*rx_packets = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
void mmipal_set_tx_qos_tid(uint8_t tid)
|
||||
{
|
||||
struct mmipal_data *data = mmipal_get_data();
|
||||
bool ok = tcpip_init_done;
|
||||
MMOSAL_ASSERT(ok);
|
||||
mmnetif_set_tx_qos_tid(&data->lwip_mmnetif, tid);
|
||||
}
|
||||
|
||||
enum mmipal_link_state mmipal_get_link_state(void)
|
||||
{
|
||||
struct mmipal_data *data = mmipal_get_data();
|
||||
return data->ip_link_state;
|
||||
}
|
||||
|
||||
static enum mmipal_status mmipal_get_local_addr_(ip_addr_t *local_addr, const ip_addr_t *dest_addr)
|
||||
{
|
||||
struct mmipal_data *data = mmipal_get_data();
|
||||
struct netif *netif = &data->lwip_mmnetif;
|
||||
#if LWIP_IPV6
|
||||
if (IP_IS_V6(dest_addr)) {
|
||||
const ip_addr_t *src_addr = ip6_select_source_address(netif, ip_2_ip6(dest_addr));
|
||||
if (src_addr == NULL) {
|
||||
return MMIPAL_NO_LINK;
|
||||
}
|
||||
ip_addr_copy(*local_addr, *src_addr);
|
||||
return MMIPAL_SUCCESS;
|
||||
} else {
|
||||
MM_UNUSED(dest_addr);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if LWIP_IPV4
|
||||
if (IP_IS_V4(dest_addr)) {
|
||||
ip_addr_copy(*local_addr, netif->ip_addr);
|
||||
return MMIPAL_SUCCESS;
|
||||
} else {
|
||||
MM_UNUSED(dest_addr);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !LWIP_IPV4 && !LWIP_IPV6
|
||||
MM_UNUSED(local_addr);
|
||||
MM_UNUSED(dest_addr);
|
||||
#endif
|
||||
|
||||
return MMIPAL_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
enum mmipal_status mmipal_get_local_addr(mmipal_ip_addr_t local_addr, const mmipal_ip_addr_t dest_addr)
|
||||
{
|
||||
ip_addr_t lwip_dest_addr;
|
||||
ip_addr_t lwip_local_addr;
|
||||
int ok;
|
||||
enum mmipal_status status;
|
||||
|
||||
if (dest_addr == NULL) {
|
||||
return MMIPAL_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
ok = ipaddr_aton(dest_addr, &lwip_dest_addr);
|
||||
if (!ok) {
|
||||
return MMIPAL_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
status = mmipal_get_local_addr_(&lwip_local_addr, &lwip_dest_addr);
|
||||
if (status != 0) {
|
||||
return status;
|
||||
}
|
||||
|
||||
if (ipaddr_ntoa_r(&lwip_local_addr, local_addr, MMIPAL_IPADDR_STR_MAXLEN) == NULL) {
|
||||
return MMIPAL_NO_MEM;
|
||||
} else {
|
||||
return MMIPAL_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
enum mmipal_status mmipal_set_dns_server(uint8_t index, const mmipal_ip_addr_t addr)
|
||||
{
|
||||
ip_addr_t dns_addr;
|
||||
int ok;
|
||||
|
||||
if (index >= DNS_MAX_SERVERS) {
|
||||
return MMIPAL_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
ok = ipaddr_aton(addr, &dns_addr);
|
||||
if (!ok) {
|
||||
return MMIPAL_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
dns_setserver(index, &dns_addr);
|
||||
return MMIPAL_SUCCESS;
|
||||
}
|
||||
|
||||
enum mmipal_status mmipal_get_dns_server(uint8_t index, mmipal_ip_addr_t addr)
|
||||
{
|
||||
const ip_addr_t *dns_addr;
|
||||
|
||||
if (index >= DNS_MAX_SERVERS) {
|
||||
return MMIPAL_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
dns_addr = dns_getserver(index);
|
||||
|
||||
#if LWIP_IPV4
|
||||
/* dns_getserver() returns ip_addr_any if no address configured. */
|
||||
if (!memcmp(dns_addr, &ip_addr_any, sizeof(*dns_addr))) {
|
||||
addr[0] = '\0';
|
||||
return MMIPAL_SUCCESS;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (ipaddr_ntoa_r(dns_addr, addr, MMIPAL_IPADDR_STR_MAXLEN) == NULL) {
|
||||
return MMIPAL_NO_MEM;
|
||||
} else {
|
||||
return MMIPAL_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,218 @@
|
||||
#ifdef USE_MM_IOT_ESP32
|
||||
/*
|
||||
* Copyright 2021-2023 Morse Micro
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include "mmnetif.h"
|
||||
#include "mmosal.h"
|
||||
#include "mmwlan.h"
|
||||
|
||||
#include "lwip/etharp.h"
|
||||
#include "lwip/ethip6.h"
|
||||
#include "lwip/tcpip.h"
|
||||
#if LWIP_SNMP
|
||||
#include "lwip/snmp.h"
|
||||
#endif
|
||||
|
||||
struct netif_state {
|
||||
volatile uint8_t tx_qos_tid;
|
||||
};
|
||||
|
||||
static struct netif_state *get_netif_state(struct netif *netif)
|
||||
{
|
||||
MMOSAL_ASSERT(netif->state != NULL);
|
||||
return (struct netif_state *)netif->state;
|
||||
}
|
||||
|
||||
/** pbuf wrapper around an mmpkt. */
|
||||
struct mmpkt_pbuf_wrapper {
|
||||
struct pbuf_custom p;
|
||||
struct mmpkt *pkt;
|
||||
struct mmpktview *pktview;
|
||||
};
|
||||
|
||||
LWIP_MEMPOOL_DECLARE(RX_POOL, MMPKTMEM_RX_POOL_N_BLOCKS, sizeof(struct mmpkt_pbuf_wrapper), "mmpkt_rx");
|
||||
|
||||
static void mmpkt_pbuf_wrapper_free(struct pbuf *p)
|
||||
{
|
||||
struct mmpkt_pbuf_wrapper *pbuf = (struct mmpkt_pbuf_wrapper *)p;
|
||||
if (p == NULL) {
|
||||
return;
|
||||
}
|
||||
mmpkt_close(&pbuf->pktview);
|
||||
mmpkt_release(pbuf->pkt);
|
||||
LWIP_MEMPOOL_FREE(RX_POOL, pbuf);
|
||||
}
|
||||
|
||||
static void mmnetif_rx(struct mmpkt *rxpkt, void *arg)
|
||||
{
|
||||
struct netif *netif = (struct netif *)arg;
|
||||
LWIP_ASSERT("arg NULL", netif != NULL);
|
||||
|
||||
LWIP_DEBUGF(NETIF_DEBUG, ("mmnetif: packet received\n"));
|
||||
|
||||
struct mmpkt_pbuf_wrapper *pbuf = (struct mmpkt_pbuf_wrapper *)LWIP_MEMPOOL_ALLOC(RX_POOL);
|
||||
if (pbuf != NULL) {
|
||||
struct pbuf *p;
|
||||
|
||||
pbuf->p.custom_free_function = mmpkt_pbuf_wrapper_free;
|
||||
pbuf->pkt = rxpkt;
|
||||
pbuf->pktview = mmpkt_open(pbuf->pkt);
|
||||
|
||||
p = pbuf_alloced_custom(PBUF_RAW, mmpkt_get_data_length(pbuf->pktview), PBUF_REF, &pbuf->p,
|
||||
mmpkt_get_data_start(pbuf->pktview), mmpkt_get_data_length(pbuf->pktview));
|
||||
int ret = tcpip_input(p, netif);
|
||||
if (ret == ERR_OK) {
|
||||
LINK_STATS_INC(link.recv);
|
||||
} else {
|
||||
LWIP_DEBUGF(NETIF_DEBUG, ("mmnetif: input error\n"));
|
||||
pbuf_free(p);
|
||||
LINK_STATS_INC(link.memerr);
|
||||
LINK_STATS_INC(link.drop);
|
||||
}
|
||||
} else {
|
||||
LWIP_DEBUGF(NETIF_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("mmnetif: alloc error\n"));
|
||||
LINK_STATS_INC(link.memerr);
|
||||
mmpkt_release(rxpkt);
|
||||
}
|
||||
}
|
||||
|
||||
static void mmnetif_link_state(enum mmwlan_link_state link_state, void *arg)
|
||||
{
|
||||
struct netif *netif = (struct netif *)arg;
|
||||
LWIP_ASSERT("arg NULL", netif != NULL);
|
||||
|
||||
LOCK_TCPIP_CORE();
|
||||
if (link_state == MMWLAN_LINK_DOWN) {
|
||||
LWIP_DEBUGF(NETIF_DEBUG | LWIP_DBG_LEVEL_ALL, ("mmnetif: link down\n"));
|
||||
/* Note: we cast netif_set_link_down to tcpip_callback_fn since the tcpip_callback_fn
|
||||
* has a "void *" parameter and netif_set_link_down has "struct netif *". */
|
||||
err_t err = tcpip_callback_with_block((tcpip_callback_fn)netif_set_link_down, netif, 0);
|
||||
LWIP_ASSERT("sched callback failed", err == ERR_OK);
|
||||
} else {
|
||||
LWIP_DEBUGF(NETIF_DEBUG | LWIP_DBG_LEVEL_ALL, ("mmnetif: link up\n"));
|
||||
/* Note: we cast netif_set_link_down to tcpip_callback_fn since the tcpip_callback_fn
|
||||
* has a "void *" parameter and netif_set_link_down has "struct netif *". */
|
||||
err_t err = tcpip_callback_with_block((tcpip_callback_fn)netif_set_link_up, netif, 0);
|
||||
LWIP_ASSERT("sched callback failed", err == ERR_OK);
|
||||
}
|
||||
UNLOCK_TCPIP_CORE();
|
||||
}
|
||||
|
||||
static err_t mmnetif_tx(struct netif *netif, struct pbuf *p)
|
||||
{
|
||||
struct mmpkt *pkt;
|
||||
struct mmpktview *pktview;
|
||||
enum mmwlan_status status;
|
||||
struct pbuf *walk;
|
||||
struct mmwlan_tx_metadata metadata = {
|
||||
.tid = get_netif_state(netif)->tx_qos_tid,
|
||||
};
|
||||
|
||||
status = mmwlan_tx_wait_until_ready(1000);
|
||||
if (status != MMWLAN_SUCCESS) {
|
||||
LWIP_DEBUGF(NETIF_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("mmnetif: transmit blocked\n"));
|
||||
LINK_STATS_INC(link.drop);
|
||||
return ERR_BUF;
|
||||
}
|
||||
|
||||
pkt = mmwlan_alloc_mmpkt_for_tx(p->tot_len, metadata.tid);
|
||||
if (pkt == NULL) {
|
||||
LWIP_DEBUGF(NETIF_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("mmnetif: allocation failure\n"));
|
||||
LINK_STATS_INC(link.memerr);
|
||||
return ERR_MEM;
|
||||
}
|
||||
pktview = mmpkt_open(pkt);
|
||||
for (walk = p; walk != NULL; walk = walk->next) {
|
||||
mmpkt_append_data(pktview, (const uint8_t *)walk->payload, walk->len);
|
||||
}
|
||||
mmpkt_close(&pktview);
|
||||
|
||||
status = mmwlan_tx_pkt(pkt, &metadata);
|
||||
if (status != MMWLAN_SUCCESS) {
|
||||
LWIP_DEBUGF(NETIF_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("mmnetif: error sending packet\n"));
|
||||
LINK_STATS_INC(link.drop);
|
||||
return ERR_BUF;
|
||||
}
|
||||
|
||||
LWIP_DEBUGF(NETIF_DEBUG | LWIP_DBG_LEVEL_ALL, ("mmnetif: packet sent\n"));
|
||||
LINK_STATS_INC(link.xmit);
|
||||
return ERR_OK;
|
||||
}
|
||||
|
||||
err_t mmnetif_init(struct netif *netif)
|
||||
{
|
||||
static bool initialised = false;
|
||||
if (initialised) {
|
||||
return ERR_IF;
|
||||
}
|
||||
|
||||
LWIP_MEMPOOL_INIT(RX_POOL);
|
||||
|
||||
LWIP_DEBUGF(NETIF_DEBUG | LWIP_DBG_LEVEL_ALL, ("mmnetif: initialising mmnetif\n"));
|
||||
|
||||
#if LWIP_SNMP
|
||||
NETIF_INIT_SNMP(netif, snmp_ifType_ethernet_csmacd, 1000000UL);
|
||||
#endif
|
||||
|
||||
enum mmwlan_status status;
|
||||
|
||||
/* Boot the transceiver so that we can read the MAC address. */
|
||||
struct mmwlan_boot_args boot_args = MMWLAN_BOOT_ARGS_INIT;
|
||||
status = mmwlan_boot(&boot_args);
|
||||
if (status != MMWLAN_SUCCESS) {
|
||||
LWIP_DEBUGF(NETIF_DEBUG | LWIP_DBG_LEVEL_SEVERE, ("mmwlan_boot failed with code %d\n", status));
|
||||
}
|
||||
MMOSAL_ASSERT(status == MMWLAN_SUCCESS);
|
||||
|
||||
/* Set MAC hardware address */
|
||||
netif->hwaddr_len = MMWLAN_MAC_ADDR_LEN;
|
||||
status = mmwlan_get_mac_addr(netif->hwaddr);
|
||||
MMOSAL_ASSERT(status == MMWLAN_SUCCESS);
|
||||
|
||||
netif->mtu = 1500;
|
||||
#if LWIP_IPV4 && !LWIP_IPV6
|
||||
netif->flags |= NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_IGMP;
|
||||
#else
|
||||
netif->flags |= NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_IGMP | NETIF_FLAG_MLD6;
|
||||
#endif
|
||||
|
||||
netif->state = NULL;
|
||||
netif->name[0] = 'M';
|
||||
netif->name[1] = 'M';
|
||||
|
||||
#if LWIP_IPV4
|
||||
netif->output = etharp_output;
|
||||
#endif
|
||||
#if LWIP_IPV6
|
||||
netif->output_ip6 = ethip6_output;
|
||||
#endif
|
||||
netif->linkoutput = mmnetif_tx;
|
||||
|
||||
struct netif_state *state = (struct netif_state *)mmosal_malloc(sizeof(*state));
|
||||
MMOSAL_ASSERT(state != NULL);
|
||||
state->tx_qos_tid = MMWLAN_TX_DEFAULT_QOS_TID;
|
||||
netif->state = state;
|
||||
|
||||
status = mmwlan_register_rx_pkt_cb(mmnetif_rx, netif);
|
||||
MMOSAL_ASSERT(status == MMWLAN_SUCCESS);
|
||||
status = mmwlan_register_link_state_cb(mmnetif_link_state, netif);
|
||||
MMOSAL_ASSERT(status == MMWLAN_SUCCESS);
|
||||
|
||||
printf("Morse LwIP interface initialised. MAC address %02x:%02x:%02x:%02x:%02x:%02x\n", netif->hwaddr[0], netif->hwaddr[1],
|
||||
netif->hwaddr[2], netif->hwaddr[3], netif->hwaddr[4], netif->hwaddr[5]);
|
||||
|
||||
initialised = true;
|
||||
|
||||
return ERR_OK;
|
||||
}
|
||||
|
||||
void mmnetif_set_tx_qos_tid(struct netif *netif, uint8_t tid)
|
||||
{
|
||||
MMOSAL_ASSERT(tid <= MMWLAN_MAX_QOS_TID);
|
||||
get_netif_state(netif)->tx_qos_tid = tid;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2021-2023 Morse Micro
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "lwip/err.h"
|
||||
#include "lwip/netif.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Initializer for the Morse Micro network interface */
|
||||
err_t mmnetif_init(struct netif *netif);
|
||||
|
||||
/**
|
||||
* Configure the QoS TID for the @c netif. QoS data will be sent using this TID.
|
||||
*
|
||||
* @param netif The @c netif to configure.
|
||||
* @param tid The TID value to set.
|
||||
*/
|
||||
void mmnetif_set_tx_qos_tid(struct netif *netif, uint8_t tid);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,563 @@
|
||||
#ifdef USE_MM_IOT_ESP32
|
||||
/*
|
||||
* Copyright 2021-2023 Morse Micro
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include "esp_debug_helpers.h"
|
||||
#include "esp_private/startup_internal.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/queue.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/timers.h"
|
||||
#include "rom/ets_sys.h"
|
||||
|
||||
#include "mmhal.h"
|
||||
#include "mmosal.h"
|
||||
|
||||
/* --------------------------------------------------------------------------------------------- */
|
||||
|
||||
/** Maximum number of failure records to store (must be a power of 2). */
|
||||
#define MAX_FAILURE_RECORDS 4
|
||||
|
||||
/** Fast implementation of _x % _m where _m is a power of 2. */
|
||||
#define FAST_MOD(_x, _m) ((_x) & ((_m)-1))
|
||||
|
||||
/** Duration to delay before resetting the device on assert. */
|
||||
#define DELAY_BEFORE_RESET_MS 1000
|
||||
|
||||
/** Data structure for assertion information to be preserved. */
|
||||
struct mmosal_preserved_failure_info {
|
||||
/** Magic number, to check if the info is valid. */
|
||||
uint32_t magic;
|
||||
|
||||
/** Number of failures recorded. */
|
||||
uint32_t failure_count;
|
||||
|
||||
/** Number of most recently displayed failure. */
|
||||
uint32_t displayed_failure_count;
|
||||
|
||||
/** Preserved information from the most recent failure(s). */
|
||||
struct mmosal_failure_info info[MAX_FAILURE_RECORDS];
|
||||
};
|
||||
|
||||
/** Magic number to put in @c mmosal_assert_info.magic to indicate that the assertion info
|
||||
* is valid. */
|
||||
#define ASSERT_INFO_MAGIC (0xabcd1234)
|
||||
|
||||
/* Persistent assertion info. Linker script should put this into memory that is not
|
||||
* zeroed on boot. Be careful to update linker script if renaming. */
|
||||
struct mmosal_preserved_failure_info preserved_failure_info __attribute__((section(".noinit")));
|
||||
|
||||
void mmosal_log_failure_info(const struct mmosal_failure_info *info)
|
||||
{
|
||||
uint32_t record_num;
|
||||
|
||||
if (preserved_failure_info.magic != ASSERT_INFO_MAGIC) {
|
||||
preserved_failure_info.failure_count = 0;
|
||||
preserved_failure_info.displayed_failure_count = 0;
|
||||
}
|
||||
|
||||
preserved_failure_info.magic = ASSERT_INFO_MAGIC;
|
||||
record_num = FAST_MOD(preserved_failure_info.failure_count, MAX_FAILURE_RECORDS);
|
||||
preserved_failure_info.failure_count++;
|
||||
memcpy(&preserved_failure_info.info[record_num], info, sizeof(*info));
|
||||
}
|
||||
|
||||
static void mmosal_dump_failure_info(void)
|
||||
{
|
||||
unsigned first_failure_num = preserved_failure_info.displayed_failure_count;
|
||||
unsigned new_failure_count = preserved_failure_info.failure_count - preserved_failure_info.displayed_failure_count;
|
||||
unsigned failure_offset;
|
||||
|
||||
if (new_failure_count >= MAX_FAILURE_RECORDS) {
|
||||
first_failure_num = FAST_MOD(preserved_failure_info.failure_count, MAX_FAILURE_RECORDS);
|
||||
new_failure_count = MAX_FAILURE_RECORDS;
|
||||
}
|
||||
|
||||
for (failure_offset = 0; failure_offset < new_failure_count; failure_offset++) {
|
||||
unsigned ii;
|
||||
unsigned idx = FAST_MOD(first_failure_num + failure_offset, MAX_FAILURE_RECORDS);
|
||||
struct mmosal_failure_info *info = &preserved_failure_info.info[idx];
|
||||
|
||||
ets_printf("Failure %u logged at pc 0x%08lx, lr 0x%08lx, line %ld in %08lx\n", first_failure_num + failure_offset,
|
||||
info->pc, info->lr, info->line, info->fileid);
|
||||
|
||||
for (ii = 0; ii < sizeof(info->platform_info) / sizeof(info->platform_info[0]); ii++) {
|
||||
ets_printf(" 0x%08lx\n", info->platform_info[ii]);
|
||||
}
|
||||
}
|
||||
|
||||
preserved_failure_info.displayed_failure_count = preserved_failure_info.failure_count;
|
||||
}
|
||||
|
||||
void mmosal_impl_assert(void)
|
||||
{
|
||||
ets_printf("MMOSAL Assert, CPU %d (current core) backtrace", xPortGetCoreID());
|
||||
(void)esp_backtrace_print(100);
|
||||
#ifdef HALT_ON_ASSERT
|
||||
if (preserved_failure_info.magic == ASSERT_INFO_MAGIC) {
|
||||
mmosal_dump_failure_info();
|
||||
}
|
||||
mmosal_disable_interrupts();
|
||||
mmhal_log_flush();
|
||||
MMPORT_BREAKPOINT();
|
||||
#else
|
||||
mmosal_task_sleep(DELAY_BEFORE_RESET_MS);
|
||||
mmhal_reset();
|
||||
#endif
|
||||
while (1) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Function to be called as part of the secondary initialization. See [System
|
||||
* Initialization](https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-guides/startup.html#system-initialization)
|
||||
* for more information. */
|
||||
ESP_SYSTEM_INIT_FN(mmosal_dump_failure_info, BIT(0), 999)
|
||||
{
|
||||
if (preserved_failure_info.magic == ASSERT_INFO_MAGIC) {
|
||||
mmosal_dump_failure_info();
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------------------------- */
|
||||
|
||||
void *mmosal_malloc_(size_t size)
|
||||
{
|
||||
return pvPortMalloc(size);
|
||||
}
|
||||
|
||||
#ifdef MMOSAL_TRACK_ALLOCATIONS
|
||||
void *mmosal_malloc_dbg(size_t size, const char *name, unsigned line_number)
|
||||
{
|
||||
return pvPortMalloc_dbg(size, name, line_number);
|
||||
}
|
||||
#else
|
||||
void *mmosal_malloc_dbg(size_t size, const char *name, unsigned line_number)
|
||||
{
|
||||
(void)name;
|
||||
(void)line_number;
|
||||
return pvPortMalloc(size);
|
||||
}
|
||||
#endif
|
||||
|
||||
void mmosal_free(void *p)
|
||||
{
|
||||
vPortFree(p);
|
||||
}
|
||||
|
||||
void *mmosal_realloc(void *ptr, size_t size)
|
||||
{
|
||||
return realloc(ptr, size);
|
||||
}
|
||||
|
||||
void *mmosal_calloc(size_t nitems, size_t size)
|
||||
{
|
||||
void *ptr = pvPortMalloc(nitems * size);
|
||||
if (ptr == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
memset(ptr, 0, nitems * size);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------------------------- */
|
||||
|
||||
struct mmosal_task_arg {
|
||||
mmosal_task_fn_t task_fn;
|
||||
void *task_fn_arg;
|
||||
};
|
||||
|
||||
void mmosal_task_main(void *arg)
|
||||
{
|
||||
struct mmosal_task_arg task_arg = *(struct mmosal_task_arg *)arg;
|
||||
mmosal_free(arg);
|
||||
task_arg.task_fn(task_arg.task_fn_arg);
|
||||
mmosal_task_delete(NULL);
|
||||
}
|
||||
|
||||
struct mmosal_task *mmosal_task_create(mmosal_task_fn_t task_fn, void *argument, enum mmosal_task_priority priority,
|
||||
unsigned stack_size_u32, const char *name)
|
||||
{
|
||||
TaskHandle_t handle;
|
||||
UBaseType_t freertos_priority = tskIDLE_PRIORITY + priority;
|
||||
|
||||
struct mmosal_task_arg *task_arg = (struct mmosal_task_arg *)mmosal_malloc(sizeof(*task_arg));
|
||||
if (task_arg == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
task_arg->task_fn = task_fn;
|
||||
task_arg->task_fn_arg = argument;
|
||||
|
||||
BaseType_t result = xTaskCreate(mmosal_task_main, name, stack_size_u32 * 4, task_arg, freertos_priority, &handle);
|
||||
if (result == pdFAIL) {
|
||||
mmosal_free(task_arg);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return (struct mmosal_task *)handle;
|
||||
}
|
||||
|
||||
void mmosal_task_delete(struct mmosal_task *task)
|
||||
{
|
||||
vTaskDelete((TaskHandle_t)task);
|
||||
}
|
||||
|
||||
/*
|
||||
* Warning: this function should not be used since eTaskGetState() is not a reliable
|
||||
* means of testing whether a task has completed.
|
||||
*
|
||||
* This function will be removed in future.
|
||||
*/
|
||||
void mmosal_task_join(struct mmosal_task *task)
|
||||
{
|
||||
while (eTaskGetState((TaskHandle_t)task) != eDeleted) {
|
||||
mmosal_task_sleep(10);
|
||||
}
|
||||
}
|
||||
|
||||
struct mmosal_task *mmosal_task_get_active(void)
|
||||
{
|
||||
return (struct mmosal_task *)xTaskGetCurrentTaskHandle();
|
||||
}
|
||||
|
||||
void mmosal_task_yield(void)
|
||||
{
|
||||
taskYIELD();
|
||||
}
|
||||
|
||||
void mmosal_task_sleep(uint32_t duration_ms)
|
||||
{
|
||||
vTaskDelay(duration_ms / portTICK_PERIOD_MS);
|
||||
}
|
||||
|
||||
static portMUX_TYPE task_spinlock = portMUX_INITIALIZER_UNLOCKED;
|
||||
|
||||
void mmosal_task_enter_critical(void)
|
||||
{
|
||||
taskENTER_CRITICAL(&task_spinlock);
|
||||
}
|
||||
|
||||
void mmosal_task_exit_critical(void)
|
||||
{
|
||||
taskEXIT_CRITICAL(&task_spinlock);
|
||||
}
|
||||
|
||||
void mmosal_disable_interrupts(void)
|
||||
{
|
||||
taskDISABLE_INTERRUPTS();
|
||||
}
|
||||
|
||||
void mmosal_enable_interrupts(void)
|
||||
{
|
||||
taskENABLE_INTERRUPTS();
|
||||
}
|
||||
|
||||
const char *mmosal_task_name(void)
|
||||
{
|
||||
TaskHandle_t t = xTaskGetCurrentTaskHandle();
|
||||
return pcTaskGetName(t);
|
||||
}
|
||||
|
||||
bool mmosal_task_wait_for_notification(uint32_t timeout_ms)
|
||||
{
|
||||
TickType_t wait = portMAX_DELAY;
|
||||
if (timeout_ms < UINT32_MAX) {
|
||||
wait = pdMS_TO_TICKS(timeout_ms);
|
||||
}
|
||||
uint32_t ret = ulTaskNotifyTake(pdTRUE, /* Act as binary semaphore */
|
||||
wait);
|
||||
return (ret != 0);
|
||||
}
|
||||
|
||||
void mmosal_task_notify(struct mmosal_task *task)
|
||||
{
|
||||
xTaskNotifyGive((TaskHandle_t)task);
|
||||
}
|
||||
|
||||
void mmosal_task_notify_from_isr(struct mmosal_task *task)
|
||||
{
|
||||
BaseType_t higher_priority_task_woken = pdFALSE;
|
||||
vTaskNotifyGiveFromISR((TaskHandle_t)task, &higher_priority_task_woken);
|
||||
portYIELD_FROM_ISR(higher_priority_task_woken);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------------------------- */
|
||||
|
||||
struct mmosal_mutex *mmosal_mutex_create(const char *name)
|
||||
{
|
||||
struct mmosal_mutex *mutex = (struct mmosal_mutex *)xSemaphoreCreateMutex();
|
||||
#if (configUSE_TRACE_FACILITY == 1) && defined(ENABLE_TRACEALYZER) && ENABLE_TRACEALYZER
|
||||
if (name != NULL) {
|
||||
vTraceSetMutexName(mutex, name);
|
||||
}
|
||||
#else
|
||||
(void)name;
|
||||
#endif
|
||||
return mutex;
|
||||
}
|
||||
|
||||
void mmosal_mutex_delete(struct mmosal_mutex *mutex)
|
||||
{
|
||||
if (mutex != NULL) {
|
||||
vQueueDelete((SemaphoreHandle_t)mutex);
|
||||
}
|
||||
}
|
||||
|
||||
bool mmosal_mutex_get(struct mmosal_mutex *mutex, uint32_t timeout_ms)
|
||||
{
|
||||
uint32_t timeout_ticks = portMAX_DELAY;
|
||||
if (timeout_ms != UINT32_MAX) {
|
||||
timeout_ticks = timeout_ms / portTICK_PERIOD_MS;
|
||||
}
|
||||
return (xSemaphoreTake((SemaphoreHandle_t)mutex, timeout_ticks) == pdPASS);
|
||||
}
|
||||
|
||||
bool mmosal_mutex_release(struct mmosal_mutex *mutex)
|
||||
{
|
||||
return (xSemaphoreGive((SemaphoreHandle_t)mutex) == pdPASS);
|
||||
}
|
||||
|
||||
bool mmosal_mutex_is_held_by_active_task(struct mmosal_mutex *mutex)
|
||||
{
|
||||
return xSemaphoreGetMutexHolder((SemaphoreHandle_t)mutex) == xTaskGetCurrentTaskHandle();
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------------------------- */
|
||||
|
||||
struct mmosal_sem *mmosal_sem_create(unsigned max_count, unsigned initial_count, const char *name)
|
||||
{
|
||||
struct mmosal_sem *sem = (struct mmosal_sem *)xSemaphoreCreateCounting(max_count, initial_count);
|
||||
#if (configUSE_TRACE_FACILITY == 1) && defined(ENABLE_TRACEALYZER) && ENABLE_TRACEALYZER
|
||||
if (name != NULL) {
|
||||
vTraceSetSemaphoreName(sem, name);
|
||||
}
|
||||
#else
|
||||
(void)name;
|
||||
#endif
|
||||
return sem;
|
||||
}
|
||||
|
||||
void mmosal_sem_delete(struct mmosal_sem *sem)
|
||||
{
|
||||
vQueueDelete((SemaphoreHandle_t)sem);
|
||||
}
|
||||
|
||||
bool mmosal_sem_give(struct mmosal_sem *sem)
|
||||
{
|
||||
return xSemaphoreGive((SemaphoreHandle_t)sem);
|
||||
}
|
||||
|
||||
bool mmosal_sem_give_from_isr(struct mmosal_sem *sem)
|
||||
{
|
||||
BaseType_t task_woken = false;
|
||||
BaseType_t ret = xSemaphoreGiveFromISR((SemaphoreHandle_t)sem, &task_woken);
|
||||
if (ret == pdPASS) {
|
||||
portYIELD_FROM_ISR(task_woken);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool mmosal_sem_wait(struct mmosal_sem *sem, uint32_t timeout_ms)
|
||||
{
|
||||
uint32_t timeout_ticks = portMAX_DELAY;
|
||||
if (timeout_ms != UINT32_MAX) {
|
||||
timeout_ticks = timeout_ms / portTICK_PERIOD_MS;
|
||||
}
|
||||
return (xSemaphoreTake((SemaphoreHandle_t)sem, timeout_ticks) == pdPASS);
|
||||
}
|
||||
|
||||
uint32_t mmosal_sem_get_count(struct mmosal_sem *sem)
|
||||
{
|
||||
return uxSemaphoreGetCount((SemaphoreHandle_t)sem);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------------------------- */
|
||||
|
||||
struct mmosal_semb *mmosal_semb_create(const char *name)
|
||||
{
|
||||
struct mmosal_semb *semb = (struct mmosal_semb *)xSemaphoreCreateBinary();
|
||||
#if (configUSE_TRACE_FACILITY == 1) && defined(ENABLE_TRACEALYZER) && ENABLE_TRACEALYZER
|
||||
if (name != NULL) {
|
||||
vTraceSetSemaphoreName(semb, name);
|
||||
}
|
||||
#else
|
||||
(void)name;
|
||||
#endif
|
||||
return semb;
|
||||
}
|
||||
|
||||
void mmosal_semb_delete(struct mmosal_semb *semb)
|
||||
{
|
||||
vQueueDelete((SemaphoreHandle_t)semb);
|
||||
}
|
||||
|
||||
bool mmosal_semb_give(struct mmosal_semb *semb)
|
||||
{
|
||||
return (xSemaphoreGive((SemaphoreHandle_t)semb) == pdPASS);
|
||||
}
|
||||
|
||||
bool mmosal_semb_give_from_isr(struct mmosal_semb *semb)
|
||||
{
|
||||
BaseType_t task_woken = pdFALSE;
|
||||
BaseType_t ret = xSemaphoreGiveFromISR((SemaphoreHandle_t)semb, &task_woken);
|
||||
if (ret == pdPASS) {
|
||||
portYIELD_FROM_ISR(task_woken);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool mmosal_semb_wait(struct mmosal_semb *semb, uint32_t timeout_ms)
|
||||
{
|
||||
uint32_t timeout_ticks = portMAX_DELAY;
|
||||
if (timeout_ms != UINT32_MAX) {
|
||||
timeout_ticks = timeout_ms / portTICK_PERIOD_MS;
|
||||
}
|
||||
return (xSemaphoreTake((SemaphoreHandle_t)semb, timeout_ticks) == pdPASS);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------------------------- */
|
||||
|
||||
struct mmosal_queue *mmosal_queue_create(size_t num_items, size_t item_size, const char *name)
|
||||
{
|
||||
struct mmosal_queue *queue = (struct mmosal_queue *)xQueueCreate(num_items, item_size);
|
||||
#if (configUSE_TRACE_FACILITY == 1) && defined(ENABLE_TRACEALYZER) && ENABLE_TRACEALYZER
|
||||
if (name != NULL) {
|
||||
vTraceSetQueueName(queue, name);
|
||||
}
|
||||
#else
|
||||
(void)name;
|
||||
#endif
|
||||
return queue;
|
||||
}
|
||||
|
||||
void mmosal_queue_delete(struct mmosal_queue *queue)
|
||||
{
|
||||
vQueueDelete((SemaphoreHandle_t)queue);
|
||||
}
|
||||
|
||||
bool mmosal_queue_pop(struct mmosal_queue *queue, void *item, uint32_t timeout_ms)
|
||||
{
|
||||
uint32_t timeout_ticks = portMAX_DELAY;
|
||||
if (timeout_ms != UINT32_MAX) {
|
||||
timeout_ticks = timeout_ms / portTICK_PERIOD_MS;
|
||||
}
|
||||
return (xQueueReceive((SemaphoreHandle_t)queue, item, timeout_ticks) == pdPASS);
|
||||
}
|
||||
|
||||
bool mmosal_queue_push(struct mmosal_queue *queue, const void *item, uint32_t timeout_ms)
|
||||
{
|
||||
uint32_t timeout_ticks = portMAX_DELAY;
|
||||
if (timeout_ms != UINT32_MAX) {
|
||||
timeout_ticks = timeout_ms / portTICK_PERIOD_MS;
|
||||
}
|
||||
return (xQueueSendToBack((SemaphoreHandle_t)queue, item, timeout_ticks) == pdPASS);
|
||||
}
|
||||
|
||||
bool mmosal_queue_pop_from_isr(struct mmosal_queue *queue, void *item)
|
||||
{
|
||||
BaseType_t task_woken = pdFALSE;
|
||||
if (xQueueReceiveFromISR((SemaphoreHandle_t)queue, item, &task_woken) == pdTRUE) {
|
||||
portYIELD_FROM_ISR(task_woken);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool mmosal_queue_push_from_isr(struct mmosal_queue *queue, const void *item)
|
||||
{
|
||||
BaseType_t task_woken = pdFALSE;
|
||||
if (xQueueSendToBackFromISR((SemaphoreHandle_t)queue, item, &task_woken) == pdTRUE) {
|
||||
portYIELD_FROM_ISR(task_woken);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------------------------- */
|
||||
|
||||
uint32_t mmosal_get_time_ms(void)
|
||||
{
|
||||
return xTaskGetTickCount() * portTICK_PERIOD_MS;
|
||||
}
|
||||
|
||||
uint32_t mmosal_get_time_ticks(void)
|
||||
{
|
||||
return xTaskGetTickCount();
|
||||
}
|
||||
|
||||
uint32_t mmosal_ticks_per_second(void)
|
||||
{
|
||||
return portTICK_PERIOD_MS * 1000;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------------------------- */
|
||||
|
||||
struct mmosal_timer *mmosal_timer_create(const char *name, uint32_t timer_period, bool auto_reload, void *arg,
|
||||
timer_callback_t callback)
|
||||
{
|
||||
/*
|
||||
* The software timer callback functions execute in the context of a task that is
|
||||
* created automatically when the FreeRTOS scheduler is started. Therefore, it is essential that
|
||||
* software timer callback functions never call FreeRTOS API functions that will result in the
|
||||
* calling task entering the Blocked state. It is ok to call functions such as xQueueReceive(), but
|
||||
* only if the function’s xTicksToWait parameter (which specifies the function’s block time) is set
|
||||
* to 0. It is not ok to call functions such as vTaskDelay(), as calling vTaskDelay() will always
|
||||
* place the calling task into the Blocked state.
|
||||
*/
|
||||
return (struct mmosal_timer *)xTimerCreate(name, pdMS_TO_TICKS(timer_period), (UBaseType_t)auto_reload, arg,
|
||||
(TimerCallbackFunction_t)callback);
|
||||
}
|
||||
|
||||
void mmosal_timer_delete(struct mmosal_timer *timer)
|
||||
{
|
||||
if (timer != NULL) {
|
||||
BaseType_t ret = xTimerDelete((TimerHandle_t)timer, 0);
|
||||
configASSERT(ret == pdPASS);
|
||||
}
|
||||
}
|
||||
|
||||
bool mmosal_timer_start(struct mmosal_timer *timer)
|
||||
{
|
||||
BaseType_t ret = xTimerStart((TimerHandle_t)timer, 0);
|
||||
|
||||
return (ret == pdPASS);
|
||||
}
|
||||
|
||||
bool mmosal_timer_stop(struct mmosal_timer *timer)
|
||||
{
|
||||
BaseType_t ret = xTimerStop((TimerHandle_t)timer, 0);
|
||||
|
||||
return (ret == pdPASS);
|
||||
}
|
||||
|
||||
bool mmosal_timer_change_period(struct mmosal_timer *timer, uint32_t new_period)
|
||||
{
|
||||
BaseType_t ret = xTimerChangePeriod((TimerHandle_t)timer, pdMS_TO_TICKS(new_period), 0);
|
||||
|
||||
return (ret == pdPASS);
|
||||
}
|
||||
|
||||
void *mmosal_timer_get_arg(struct mmosal_timer *timer)
|
||||
{
|
||||
return pvTimerGetTimerID((TimerHandle_t)timer);
|
||||
}
|
||||
|
||||
bool mmosal_is_timer_active(struct mmosal_timer *timer)
|
||||
{
|
||||
BaseType_t ret = xTimerIsTimerActive((TimerHandle_t)timer);
|
||||
|
||||
return (ret != pdFALSE);
|
||||
}
|
||||
|
||||
#endif /* USE_MM_IOT_ESP32 */
|
||||
@@ -0,0 +1,245 @@
|
||||
#ifdef USE_MM_IOT_ESP32
|
||||
/*
|
||||
* Copyright 2024 Morse Micro
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdatomic.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "mmhal.h"
|
||||
#include "mmosal.h"
|
||||
#include "mmpkt.h"
|
||||
#include "mmpkt_list.h"
|
||||
#include "mmutils.h"
|
||||
|
||||
/* MMPKTMEM_TX_POOL_N_BLOCKS and MMPKTMEM_RX_POOL_N_BLOCKS provide an upper bound on the number
|
||||
* of packets we will allocate in the transmit and receive directions respectively. */
|
||||
|
||||
#ifndef MMPKTMEM_TX_POOL_N_BLOCKS
|
||||
#error MMPKTMEM_TX_POOL_N_BLOCKS not defined
|
||||
#endif
|
||||
|
||||
#ifndef MMPKTMEM_RX_POOL_N_BLOCKS
|
||||
#error MMPKTMEM_RX_POOL_N_BLOCKS not defined
|
||||
#endif
|
||||
|
||||
/* Packet pool for data/management frames configuration. */
|
||||
#define TX_DATA_POOL_UNPAUSE_THRESHOLD (MMPKTMEM_TX_POOL_N_BLOCKS - 2)
|
||||
#define TX_DATA_POOL_PAUSE_THRESHOLD (MMPKTMEM_TX_POOL_N_BLOCKS - 1)
|
||||
|
||||
/* Packet pool for commands configuration. */
|
||||
#define TX_COMMAND_POOL_BLOCK_SIZE (256)
|
||||
#define TX_COMMAND_POOL_N_BLOCKS (2)
|
||||
|
||||
#ifndef MMPKT_LOG
|
||||
#define MMPKT_LOG(...) printf(__VA_ARGS__)
|
||||
#endif
|
||||
|
||||
struct pktmem_data {
|
||||
/** Count of allocated tx packets (excluding command pool -- see below). */
|
||||
volatile atomic_int_least32_t tx_data_pool_allocated;
|
||||
/** Boolean tracking whether the data path is currently paused. */
|
||||
volatile atomic_uint_fast8_t tx_data_pool_tx_paused;
|
||||
/** Count of allocated rx packets. */
|
||||
volatile atomic_int_least32_t rx_pool_allocated;
|
||||
|
||||
/** Command pool free (unallocated) packet list. */
|
||||
struct mmpkt_list tx_command_pool_free_list;
|
||||
/** Statically allocated memory for the command pool. */
|
||||
uint8_t tx_command_pool[TX_COMMAND_POOL_BLOCK_SIZE * TX_COMMAND_POOL_N_BLOCKS];
|
||||
|
||||
/** Flow control callback function pointer. */
|
||||
mmhal_wlan_pktmem_tx_flow_control_cb_t tx_flow_control_cb;
|
||||
};
|
||||
|
||||
static struct pktmem_data pktmem;
|
||||
|
||||
void mmhal_wlan_pktmem_init(struct mmhal_wlan_pktmem_init_args *args)
|
||||
{
|
||||
unsigned ii;
|
||||
|
||||
memset(&pktmem, 0, sizeof(pktmem));
|
||||
|
||||
pktmem.tx_flow_control_cb = args->tx_flow_control_cb;
|
||||
|
||||
/* Initialize the free (unallocated) packet list of the transmit command pool. */
|
||||
for (ii = 0; ii < TX_COMMAND_POOL_N_BLOCKS; ii++) {
|
||||
size_t offset = TX_COMMAND_POOL_BLOCK_SIZE * ii;
|
||||
mmpkt_list_append(&pktmem.tx_command_pool_free_list, (struct mmpkt *)(pktmem.tx_command_pool + offset));
|
||||
}
|
||||
}
|
||||
|
||||
void mmhal_wlan_pktmem_deinit(void)
|
||||
{
|
||||
size_t ii;
|
||||
|
||||
/* If there is still memory allocated, allow some time for other threads to clean up. */
|
||||
for (ii = 0; ii < 100; ii++) {
|
||||
if ((pktmem.tx_command_pool_free_list.len | pktmem.tx_data_pool_allocated | pktmem.tx_data_pool_allocated) == 0) {
|
||||
break;
|
||||
}
|
||||
mmosal_task_sleep(10);
|
||||
}
|
||||
|
||||
/* Check for memory leaks. */
|
||||
if (pktmem.tx_data_pool_allocated != 0) {
|
||||
MMPKT_LOG("Potential memory leak: %d %s pool allocations at deinit\n", (int)pktmem.tx_data_pool_allocated, "data");
|
||||
}
|
||||
|
||||
if (pktmem.tx_command_pool_free_list.len != TX_COMMAND_POOL_N_BLOCKS) {
|
||||
MMPKT_LOG("Potential memory leak: %d %s pool allocations at deinit\n",
|
||||
TX_COMMAND_POOL_N_BLOCKS - (int)pktmem.tx_command_pool_free_list.len, "command");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* --------------------------------------------------------------------------------------
|
||||
* Command pool
|
||||
* --------------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
static void tx_command_reserved_free(void *mmpkt)
|
||||
{
|
||||
struct mmpkt *pkt = (struct mmpkt *)mmpkt;
|
||||
MMOSAL_TASK_ENTER_CRITICAL();
|
||||
mmpkt_list_append(&pktmem.tx_command_pool_free_list, pkt);
|
||||
MMOSAL_TASK_EXIT_CRITICAL();
|
||||
}
|
||||
|
||||
static const struct mmpkt_ops tx_command_pool_ops = {
|
||||
.free_mmpkt = tx_command_reserved_free,
|
||||
};
|
||||
|
||||
static struct mmpkt *alloc_pkt_from_list(struct mmpkt_list *list, uint32_t pktbufsize, uint32_t space_at_start,
|
||||
uint32_t space_at_end, uint32_t metadata_length)
|
||||
{
|
||||
struct mmpkt *mmpkt_buf;
|
||||
struct mmpkt *mmpkt;
|
||||
|
||||
MMOSAL_TASK_ENTER_CRITICAL();
|
||||
mmpkt_buf = mmpkt_list_dequeue(list);
|
||||
MMOSAL_TASK_EXIT_CRITICAL();
|
||||
|
||||
if (mmpkt_buf == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
mmpkt = mmpkt_init_buf((uint8_t *)mmpkt_buf, pktbufsize, space_at_start, space_at_end, metadata_length, &tx_command_pool_ops);
|
||||
if (mmpkt == NULL) {
|
||||
/* Command was too big for the reserved buffer. Return the reserved buffer. */
|
||||
tx_command_reserved_free(mmpkt_buf);
|
||||
}
|
||||
|
||||
return mmpkt;
|
||||
}
|
||||
|
||||
static struct mmpkt *command_pool_alloc(uint32_t space_at_start, uint32_t space_at_end, uint32_t metadata_length)
|
||||
{
|
||||
return alloc_pkt_from_list(&pktmem.tx_command_pool_free_list, TX_COMMAND_POOL_BLOCK_SIZE, space_at_start, space_at_end,
|
||||
metadata_length);
|
||||
}
|
||||
|
||||
/*
|
||||
* --------------------------------------------------------------------------------------
|
||||
* Data pool
|
||||
* --------------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
static void tx_data_pool_pkt_free(void *mmpkt)
|
||||
{
|
||||
atomic_int_least32_t old_value = atomic_fetch_sub(&pktmem.tx_data_pool_allocated, 1);
|
||||
MMOSAL_ASSERT(old_value > 0);
|
||||
mmosal_free(mmpkt);
|
||||
|
||||
if (pktmem.tx_data_pool_allocated < TX_DATA_POOL_UNPAUSE_THRESHOLD) {
|
||||
atomic_uint_fast8_t old_tx_paused = atomic_exchange(&pktmem.tx_data_pool_tx_paused, 0);
|
||||
if (old_tx_paused) {
|
||||
pktmem.tx_flow_control_cb(MMWLAN_TX_READY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static const struct mmpkt_ops tx_data_pool_pkt_ops = {
|
||||
.free_mmpkt = tx_data_pool_pkt_free,
|
||||
};
|
||||
|
||||
struct mmpkt *mmhal_wlan_alloc_mmpkt_for_tx(uint8_t pkt_class, uint32_t space_at_start, uint32_t space_at_end,
|
||||
uint32_t metadata_length)
|
||||
{
|
||||
atomic_int_least32_t old_value;
|
||||
struct mmpkt *mmpkt;
|
||||
|
||||
/* For command packets, try allocating from the command pool first. If that fails then
|
||||
* we proceed to allocate from the data pool. */
|
||||
if (pkt_class == MMHAL_WLAN_PKT_COMMAND) {
|
||||
mmpkt = command_pool_alloc(space_at_start, space_at_end, metadata_length);
|
||||
if (mmpkt != NULL) {
|
||||
return mmpkt;
|
||||
}
|
||||
}
|
||||
|
||||
old_value = atomic_fetch_add(&pktmem.tx_data_pool_allocated, 1);
|
||||
|
||||
if (old_value >= MMPKTMEM_TX_POOL_N_BLOCKS) {
|
||||
/* Maximum allocations reached. Do not attempt to increase further. */
|
||||
atomic_fetch_sub(&pktmem.tx_data_pool_allocated, 1);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
mmpkt = mmpkt_alloc_on_heap(space_at_start, space_at_end, metadata_length);
|
||||
if (mmpkt == NULL) {
|
||||
atomic_fetch_sub(&pktmem.tx_data_pool_allocated, 1);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
mmpkt->ops = &tx_data_pool_pkt_ops;
|
||||
|
||||
if (pktmem.tx_data_pool_allocated > TX_DATA_POOL_PAUSE_THRESHOLD) {
|
||||
atomic_uint_fast8_t old_tx_paused = atomic_exchange(&pktmem.tx_data_pool_tx_paused, 1);
|
||||
if (!old_tx_paused) {
|
||||
pktmem.tx_flow_control_cb(MMWLAN_TX_PAUSED);
|
||||
}
|
||||
}
|
||||
|
||||
return mmpkt;
|
||||
}
|
||||
|
||||
static void rx_pkt_free(void *mmpkt)
|
||||
{
|
||||
if (mmpkt != NULL) {
|
||||
atomic_fetch_sub(&pktmem.rx_pool_allocated, 1);
|
||||
mmosal_free(mmpkt);
|
||||
}
|
||||
}
|
||||
|
||||
static const struct mmpkt_ops mmpkt_rx_ops = {.free_mmpkt = rx_pkt_free};
|
||||
|
||||
struct mmpkt *mmhal_wlan_alloc_mmpkt_for_rx(uint32_t capacity, uint32_t metadata_length)
|
||||
{
|
||||
atomic_int_least32_t old_value;
|
||||
struct mmpkt *mmpkt;
|
||||
|
||||
old_value = atomic_fetch_add(&pktmem.rx_pool_allocated, 1);
|
||||
|
||||
if (old_value >= MMPKTMEM_RX_POOL_N_BLOCKS) {
|
||||
/* Maximum allocations reached. Do not attempt to increase further. */
|
||||
atomic_fetch_sub(&pktmem.rx_pool_allocated, 1);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* For now we do not put an explicit limit on the of packets buffers on the RX path. */
|
||||
mmpkt = mmpkt_alloc_on_heap(0, capacity, metadata_length);
|
||||
if (mmpkt == NULL) {
|
||||
atomic_fetch_sub(&pktmem.rx_pool_allocated, 1);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Override packet ops to use a custom free function that also decrements the
|
||||
* allocation count. */
|
||||
mmpkt->ops = &mmpkt_rx_ops;
|
||||
return mmpkt;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Copyright 2023 Morse Micro
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#define MMPORT_BREAKPOINT() while (1)
|
||||
#define MMPORT_GET_LR() (__builtin_return_address(0))
|
||||
#define MMPORT_GET_PC(_a) ((_a) = 0) // TODO
|
||||
#define MMPORT_MEM_SYNC() __sync_synchronize()
|
||||
@@ -0,0 +1,301 @@
|
||||
/*
|
||||
* Copyright 2024 Morse Micro
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @defgroup MMUTILS Morse Micro Utilities
|
||||
*
|
||||
* Utility macros and functions to improve quality of life.
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Get the minimum of 2 values.
|
||||
*
|
||||
* Note that this macro is not ideal and should be used with caution. Caveats include:
|
||||
* * The two parameters may be evaluated more than once, so should be constant values that
|
||||
* do not have side effects. For example, do NOT do `MM_MIN(a++, b)`.
|
||||
* * There are no explicit constraints on types, so be careful of comparing different integer
|
||||
* types, etc.
|
||||
*
|
||||
* @param _x The first value to compare.
|
||||
* @param _y The second value to compare.
|
||||
*
|
||||
* @returns the minimum of @p _x and @p _y.
|
||||
*/
|
||||
#define MM_MIN(_x, _y) (((_x) < (_y)) ? (_x) : (_y))
|
||||
|
||||
/**
|
||||
* Get the maximum of 2 values.
|
||||
*
|
||||
* Note that this macro is not ideal and should be used with caution. Caveats include:
|
||||
* * The two parameters may be evaluated more than once, so should be constant values that
|
||||
* do not have side effects. For example, do NOT do `MM_MAX(a++, b)`.
|
||||
* * There are no explicit constraints on types, so be careful of comparing different integer
|
||||
* types, etc.
|
||||
*
|
||||
* @param _x The first value to compare.
|
||||
* @param _y The second value to compare.
|
||||
*
|
||||
* @returns the maximum of @p _x and @p _y.
|
||||
*/
|
||||
#define MM_MAX(_x, _y) (((_x) > (_y)) ? (_x) : (_y))
|
||||
|
||||
/**
|
||||
* Round @p x up to the next multiple of @p m (where @p m is a power of 2).
|
||||
*
|
||||
* @warning @p m must be a power of 2.
|
||||
*/
|
||||
#ifndef MM_FAST_ROUND_UP
|
||||
#define MM_FAST_ROUND_UP(x, m) ((((x)-1) | ((m)-1)) + 1)
|
||||
#endif
|
||||
|
||||
/** Casts the given expression to void to avoid "unused" warnings from the compiler. */
|
||||
#define MM_UNUSED(_x) (void)(_x)
|
||||
|
||||
/** Tells the compiler to pack the structure. */
|
||||
#ifndef MM_PACKED
|
||||
#define MM_PACKED __attribute__((packed))
|
||||
#endif
|
||||
|
||||
/** Used to declare a weak symbol. */
|
||||
#ifndef MM_WEAK
|
||||
#define MM_WEAK __attribute__((weak))
|
||||
#endif
|
||||
|
||||
#ifndef MM_STATIC_ASSERT
|
||||
/**
|
||||
* Assertion check that is evaluated at compile time.
|
||||
*
|
||||
* The constant expression, @p _expression, is evaluted at compile time. If zero then
|
||||
* it triggers a compilation error and @p _message is displayed. If non-zero, no code
|
||||
* is emitted.
|
||||
*
|
||||
* @param _expression Constant expression to evaluate. If zero then a compilation error
|
||||
* is triggered.
|
||||
* @param _message Message to display on error.
|
||||
*/
|
||||
#define MM_STATIC_ASSERT(_expression, _message) _Static_assert((_expression), _message)
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Return the number of elements in the given array.
|
||||
*
|
||||
* @param _a The array to get the element count for. Note that this must be an _array_
|
||||
* and not a pointer. Beware that array-type function arguments are
|
||||
* actually treated as pointers by the compiler. Must not be NULL.
|
||||
*
|
||||
* @returns the count of elements in the given array.
|
||||
*/
|
||||
#define MM_ARRAY_COUNT(_a) (sizeof(_a) / sizeof((_a)[0]))
|
||||
|
||||
/**
|
||||
* Convert the least significant 4 bits of the given argument to a character representing their
|
||||
* hexadecimal value.
|
||||
*
|
||||
* For example, for input 0xde this will return 'E', for 0x01 it will return '1'.
|
||||
*
|
||||
* @param nibble The input nibble (upper 4 bits will be discarded).
|
||||
*
|
||||
* @return The character that represents the hexadecimal value of the lower 4 bits of @p nibble.
|
||||
* Values greater than 0x09 will be represented with upper case characters.
|
||||
*/
|
||||
static inline char mm_nibble_to_hex_char(uint8_t nibble)
|
||||
{
|
||||
nibble &= 0x0f;
|
||||
if (nibble < 0x0a) {
|
||||
return '0' + nibble;
|
||||
} else {
|
||||
return 'A' + nibble - 0x0a;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @defgroup MMUTILS_WLAN WLAN Utilities
|
||||
*
|
||||
* Utility macros and functions relating to WLAN.
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
/** Enumeration of Authentication Key Management (AKM) Suite OUIs as BE32 integers. */
|
||||
enum mm_akm_suite_oui {
|
||||
/** Open (no security) */
|
||||
MM_AKM_SUITE_NONE = 0,
|
||||
/** Pre-shared key (WFA OUI) */
|
||||
MM_AKM_SUITE_PSK = 0x506f9a02,
|
||||
/** Simultaneous Authentication of Equals (SAE) */
|
||||
MM_AKM_SUITE_SAE = 0x000fac08,
|
||||
/** OWE */
|
||||
MM_AKM_SUITE_OWE = 0x000fac12,
|
||||
/** Another suite not in this enum */
|
||||
MM_AKM_SUITE_OTHER = 1,
|
||||
};
|
||||
|
||||
/** Enumeration of Cipher Suite OUIs as BE32 integers. */
|
||||
enum mm_cipher_suite_oui {
|
||||
/** Open (no security) */
|
||||
MM_CIPHER_SUITE_AES_CCM = 0x000fac04,
|
||||
/** Another cipher suite not in this enum */
|
||||
MM_CIPHER_SUITE_OTHER = 1,
|
||||
};
|
||||
|
||||
/** Maximum number of pairwise cipher suites our parser will process. */
|
||||
#ifndef MM_RSN_INFORMATION_MAX_PAIRWISE_CIPHER_SUITES
|
||||
#define MM_RSN_INFORMATION_MAX_PAIRWISE_CIPHER_SUITES (2)
|
||||
#endif
|
||||
|
||||
/** Maximum number of AKM suites our parser will process. */
|
||||
#ifndef MM_RSN_INFORMATION_MAX_AKM_SUITES
|
||||
#define MM_RSN_INFORMATION_MAX_AKM_SUITES (2)
|
||||
#endif
|
||||
|
||||
/** Tag number of the RSN information element, in which we can find security details of the AP. */
|
||||
#define MM_RSN_INFORMATION_IE_TYPE (48)
|
||||
/** Tag number of the Vendor Specific information element. */
|
||||
#define MM_VENDOR_SPECIFIC_IE_TYPE (221)
|
||||
|
||||
/** Explicitly defined errno values to obviate the need to include errno.h. MM prefix to
|
||||
* avoid namespace collision in case errno.h gets included. */
|
||||
enum mm_errno {
|
||||
MM_ENOMEM = 12,
|
||||
MM_EFAULT = 14,
|
||||
MM_ENODEV = 19,
|
||||
MM_EINVAL = 22,
|
||||
MM_ETIMEDOUT = 110,
|
||||
};
|
||||
|
||||
/**
|
||||
* Data structure to represent information extracted from an RSN information element.
|
||||
*
|
||||
* All integers in host order.
|
||||
*/
|
||||
struct mm_rsn_information {
|
||||
/** The group cipher suite OUI. */
|
||||
uint32_t group_cipher_suite;
|
||||
/** Pairwise cipher suite OUIs. Count given by @c num_pairwise_cipher_suites. */
|
||||
uint32_t pairwise_cipher_suites[MM_RSN_INFORMATION_MAX_PAIRWISE_CIPHER_SUITES];
|
||||
/** AKM suite OUIs. Count given by @c num_akm_suites. */
|
||||
uint32_t akm_suites[MM_RSN_INFORMATION_MAX_AKM_SUITES];
|
||||
/** Number of pairwise cipher suites in @c pairwise_cipher_suites. */
|
||||
uint16_t num_pairwise_cipher_suites;
|
||||
/** Number of AKM suites in @c akm_suites. */
|
||||
uint16_t num_akm_suites;
|
||||
/** Version number of the RSN IE. */
|
||||
uint16_t version;
|
||||
/** RSN Capabilities field of the RSN IE (in host order). */
|
||||
uint16_t rsn_capabilities;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the name of the given AKM Suite as a string.
|
||||
*
|
||||
* @param akm_suite_oui The OUI of the AKM suite as a big endian integer.
|
||||
*
|
||||
* @returns the string representation.
|
||||
*/
|
||||
const char *mm_akm_suite_to_string(uint32_t akm_suite_oui);
|
||||
|
||||
/**
|
||||
* Search a list of Information Elements (IEs) from the given starting offset and find the first
|
||||
* instance of matching the given type.
|
||||
*
|
||||
* @warning A @p search_offset that is not aligned to the start of an IE header will result in
|
||||
* undefined behaviour.
|
||||
*
|
||||
* @param ies Buffer containing the information elements.
|
||||
* @param ies_len Length of @p ies
|
||||
* @param search_offset Offset to start searching from. This **must** point to a IE header.
|
||||
* @param ie_type The type of the IE to look for.
|
||||
*
|
||||
* @return If the information element is found, the offset of the start of the IE within @p ies; if
|
||||
* no match is found then -1; if the IE is found but is malformed then -2.
|
||||
*/
|
||||
int mm_find_ie_from_offset(const uint8_t *ies, uint32_t ies_len, uint32_t search_offset, uint8_t ie_type);
|
||||
|
||||
/**
|
||||
* Search a list of Information Elements (IEs) and find the first instance of matching the
|
||||
* given type.
|
||||
*
|
||||
* @param ies Buffer containing the information elements.
|
||||
* @param ies_len Length of @p ies
|
||||
* @param ie_type The type of the IE to look for.
|
||||
*
|
||||
* @return If the information element is found, the offset of the start of the IE within @p ies;
|
||||
* if no match is found then -1; if the IE is found but is malformed then -2.
|
||||
*/
|
||||
static inline int mm_find_ie(const uint8_t *ies, uint32_t ies_len, uint8_t ie_type)
|
||||
{
|
||||
return mm_find_ie_from_offset(ies, ies_len, 0, ie_type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search through the given list of Information Elements (IEs) from the given starting offset to
|
||||
* find the first Vendor Specific IE that matches the given id.
|
||||
*
|
||||
* @warning A @p search_offset that is not aligned to the start of an IE header will result in
|
||||
* undefined behaviour.
|
||||
*
|
||||
* @param[in] ies Buffer containing the information elements.
|
||||
* @param[in] ies_len Length of @p ies
|
||||
* @param[in] search_offset Offset to start searching from. This **must** point to a IE header.
|
||||
* @param[in] id Buffer containing the IE ID, usually OUI+TYPE.
|
||||
* @param[in] id_len Length of the ID.
|
||||
*
|
||||
* @return If the information element is found, the offset of the start of the IE within @p ies; if
|
||||
* no match is found then -1; if the IE is found but is malformed then -2.
|
||||
*/
|
||||
int mm_find_vendor_specific_ie_from_offset(const uint8_t *ies, uint32_t ies_len, uint32_t search_offset, const uint8_t *id,
|
||||
size_t id_len);
|
||||
|
||||
/**
|
||||
* Search through the given list of Information Elements (IEs) to find the first Vendor Specific IE
|
||||
* that matches the given id.
|
||||
*
|
||||
* @param[in] ies Buffer containing the information elements.
|
||||
* @param[in] ies_len Length of @p ies
|
||||
* @param[in] id Buffer containing the IE ID, usually OUI+TYPE.
|
||||
* @param[in] id_len Length of the ID.
|
||||
*
|
||||
* @return If the information element is found, the offset of the start of the IE within @p ies; if
|
||||
* no match is found then -1; if the IE is found but is malformed then -2.
|
||||
*/
|
||||
static inline int mm_find_vendor_specific_ie(const uint8_t *ies, uint32_t ies_len, const uint8_t *id, size_t id_len)
|
||||
{
|
||||
return mm_find_vendor_specific_ie_from_offset(ies, ies_len, 0, id, id_len);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search through the given list of information elements to find the RSN IE then parse it
|
||||
* to extract relevant information into an instance of @ref mm_rsn_information.
|
||||
*
|
||||
* @param[in] ies Buffer containing the information elements.
|
||||
* @param[in] ies_len Length of @p ies
|
||||
* @param[out] output Pointer to an instance of @ref mm_rsn_information to receive output.
|
||||
*
|
||||
* @returns -2 on parse error, -1 if the RSN IE was not found, 0 if the RSN IE was found.
|
||||
*/
|
||||
int mm_parse_rsn_information(const uint8_t *ies, uint32_t ies_len, struct mm_rsn_information *output);
|
||||
|
||||
/** @} */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
/** @} */
|
||||
@@ -0,0 +1,163 @@
|
||||
#ifdef USE_MM_IOT_ESP32
|
||||
/*
|
||||
* Copyright 2024 Morse Micro
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#include "mmutils.h"
|
||||
|
||||
const char *mm_akm_suite_to_string(uint32_t akm_suite_oui)
|
||||
{
|
||||
switch (akm_suite_oui) {
|
||||
case MM_AKM_SUITE_NONE:
|
||||
return "None";
|
||||
|
||||
case MM_AKM_SUITE_PSK:
|
||||
return "PSK";
|
||||
|
||||
case MM_AKM_SUITE_SAE:
|
||||
return "SAE";
|
||||
|
||||
case MM_AKM_SUITE_OWE:
|
||||
return "OWE";
|
||||
|
||||
default:
|
||||
return "Other";
|
||||
}
|
||||
}
|
||||
|
||||
int mm_find_ie_from_offset(const uint8_t *ies, uint32_t ies_len, uint32_t search_offset, uint8_t ie_type)
|
||||
{
|
||||
while ((search_offset + 2) <= ies_len) {
|
||||
uint8_t type = ies[search_offset];
|
||||
uint8_t length = ies[search_offset + 1];
|
||||
|
||||
if (type == ie_type) {
|
||||
if ((search_offset + 2 + length) > ies_len) {
|
||||
return -2;
|
||||
}
|
||||
return search_offset;
|
||||
}
|
||||
|
||||
search_offset += 2 + length;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
int mm_find_vendor_specific_ie_from_offset(const uint8_t *ies, uint32_t ies_len, uint32_t search_offset, const uint8_t *id,
|
||||
size_t id_len)
|
||||
{
|
||||
int offset = 0;
|
||||
|
||||
while ((search_offset + id_len) <= ies_len) {
|
||||
offset = mm_find_ie_from_offset(ies, ies_len, search_offset, MM_VENDOR_SPECIFIC_IE_TYPE);
|
||||
if (offset < 0) {
|
||||
return offset;
|
||||
}
|
||||
|
||||
uint8_t ie_type = ies[offset];
|
||||
uint8_t ie_length = ies[offset + 1];
|
||||
const uint8_t *ie_data = ies + (offset + 2);
|
||||
|
||||
if (ie_type == MM_VENDOR_SPECIFIC_IE_TYPE && id_len <= ie_length && (memcmp(id, ie_data, id_len) == 0)) {
|
||||
if (((uint32_t)offset + 2 + ie_length) > ies_len) {
|
||||
return -2;
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
search_offset = 2 + ie_length + (uint32_t)offset;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
int mm_parse_rsn_information(const uint8_t *ies, uint32_t ies_len, struct mm_rsn_information *output)
|
||||
{
|
||||
uint8_t length;
|
||||
uint16_t num_pairwise_cipher_suites;
|
||||
uint16_t num_akm_suites;
|
||||
uint16_t ii;
|
||||
|
||||
int offset = mm_find_ie(ies, ies_len, MM_RSN_INFORMATION_IE_TYPE);
|
||||
|
||||
memset(output, 0, sizeof(*output));
|
||||
|
||||
if (offset < 0) {
|
||||
return offset;
|
||||
}
|
||||
|
||||
/* Note that we rely on mm_find_ie() to validate that the IE does not extend past the end
|
||||
* of the given buffer. */
|
||||
|
||||
length = ies[offset + 1];
|
||||
offset += 2;
|
||||
|
||||
if (length < 8) {
|
||||
printf("*WRN* RSN IE too short\n");
|
||||
return -2;
|
||||
}
|
||||
|
||||
/* Skip version field */
|
||||
output->version = ies[offset] | ies[offset + 1] << 8;
|
||||
offset += 2;
|
||||
length -= 2;
|
||||
|
||||
output->group_cipher_suite = ies[offset] << 24 | ies[offset + 1] << 16 | ies[offset + 2] << 8 | ies[offset + 3];
|
||||
offset += 4;
|
||||
length -= 4;
|
||||
|
||||
num_pairwise_cipher_suites = ies[offset] | ies[offset + 1] << 8;
|
||||
offset += 2;
|
||||
length -= 2;
|
||||
|
||||
output->num_pairwise_cipher_suites = num_pairwise_cipher_suites;
|
||||
if (num_pairwise_cipher_suites > MM_RSN_INFORMATION_MAX_PAIRWISE_CIPHER_SUITES) {
|
||||
output->num_pairwise_cipher_suites = MM_RSN_INFORMATION_MAX_PAIRWISE_CIPHER_SUITES;
|
||||
}
|
||||
|
||||
if (length < 4 * num_pairwise_cipher_suites + 2) {
|
||||
printf("*WRN* RSN IE too short\n");
|
||||
return -2;
|
||||
}
|
||||
|
||||
for (ii = 0; ii < num_pairwise_cipher_suites; ii++) {
|
||||
if (ii < output->num_pairwise_cipher_suites) {
|
||||
output->pairwise_cipher_suites[ii] =
|
||||
ies[offset] << 24 | ies[offset + 1] << 16 | ies[offset + 2] << 8 | ies[offset + 3];
|
||||
}
|
||||
offset += 4;
|
||||
length -= 4;
|
||||
}
|
||||
|
||||
num_akm_suites = ies[offset] | ies[offset + 1] << 8;
|
||||
offset += 2;
|
||||
length -= 2;
|
||||
|
||||
output->num_akm_suites = num_akm_suites;
|
||||
if (num_akm_suites > MM_RSN_INFORMATION_MAX_AKM_SUITES) {
|
||||
output->num_akm_suites = MM_RSN_INFORMATION_MAX_AKM_SUITES;
|
||||
}
|
||||
|
||||
if (length < 4 * num_akm_suites + 2) {
|
||||
printf("*WRN* RSN IE too short\n");
|
||||
return -2;
|
||||
}
|
||||
|
||||
for (ii = 0; ii < num_akm_suites; ii++) {
|
||||
if (ii < output->num_akm_suites) {
|
||||
output->akm_suites[ii] = ies[offset] << 24 | ies[offset + 1] << 16 | ies[offset + 2] << 8 | ies[offset + 3];
|
||||
}
|
||||
offset += 4;
|
||||
length -= 4;
|
||||
}
|
||||
|
||||
output->rsn_capabilities = ies[offset] | ies[offset + 1] << 8;
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif /* USE_MM_IOT_ESP32 */
|
||||
@@ -0,0 +1,267 @@
|
||||
#ifdef USE_MM_IOT_ESP32
|
||||
/*
|
||||
* Copyright 2021-2023 Morse Micro
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "mmhal.h"
|
||||
#include "mmosal.h"
|
||||
|
||||
#include "driver/gpio.h"
|
||||
#include "driver/spi_common.h"
|
||||
#include "driver/spi_master.h"
|
||||
#include "esp_random.h"
|
||||
#include "esp_system.h"
|
||||
|
||||
/** 10x8bit training seq */
|
||||
#define BYTE_TRAIN 16
|
||||
|
||||
/** SPI hw interrupt handler. Must be set before enabling irq */
|
||||
static mmhal_irq_handler_t spi_irq_handler = NULL;
|
||||
|
||||
/** busy interrupt handler. Must be set before enabling irq */
|
||||
static mmhal_irq_handler_t busy_irq_handler = NULL;
|
||||
|
||||
static spi_device_handle_t spi_handle;
|
||||
|
||||
static void wlan_hal_gpio_init(void)
|
||||
{
|
||||
gpio_config_t io_conf = {};
|
||||
io_conf.intr_type = GPIO_INTR_DISABLE;
|
||||
io_conf.mode = GPIO_MODE_OUTPUT;
|
||||
io_conf.pin_bit_mask = ((1ull << CONFIG_MM_WAKE) | (1ull << CONFIG_MM_SPI_CS));
|
||||
io_conf.pull_down_en = 0;
|
||||
io_conf.pull_up_en = 0;
|
||||
gpio_config(&io_conf);
|
||||
|
||||
gpio_set_level(CONFIG_MM_WAKE, 0);
|
||||
gpio_set_level(CONFIG_MM_SPI_CS, 0);
|
||||
|
||||
io_conf.intr_type = GPIO_INTR_DISABLE;
|
||||
io_conf.mode = GPIO_MODE_INPUT;
|
||||
io_conf.pin_bit_mask = (1ull << CONFIG_MM_BUSY);
|
||||
io_conf.pull_down_en = 1;
|
||||
gpio_config(&io_conf);
|
||||
|
||||
io_conf.intr_type = GPIO_INTR_DISABLE;
|
||||
io_conf.mode = GPIO_MODE_INPUT;
|
||||
io_conf.pin_bit_mask = (1ull << CONFIG_MM_SPI_IRQ);
|
||||
io_conf.pull_down_en = 0;
|
||||
gpio_config(&io_conf);
|
||||
}
|
||||
|
||||
static void wlan_hal_spi_init(void)
|
||||
{
|
||||
esp_err_t ret;
|
||||
|
||||
spi_bus_config_t buscfg = {
|
||||
.miso_io_num = CONFIG_MM_SPI_MISO,
|
||||
.mosi_io_num = CONFIG_MM_SPI_MOSI,
|
||||
.sclk_io_num = CONFIG_MM_SPI_SCK,
|
||||
.quadwp_io_num = -1,
|
||||
.quadhd_io_num = -1,
|
||||
/* max_transfer_sz defaults to 4092 if 0 when DMA enabled, or to SOC_SPI_MAXIMUM_BUFFER_SIZE
|
||||
* if DMA is disabled. */
|
||||
.max_transfer_sz = 0,
|
||||
.flags = SPICOMMON_BUSFLAG_MASTER,
|
||||
};
|
||||
ret = spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO);
|
||||
if (ret != ESP_OK) {
|
||||
printf("spi_bus_initialize failed\n");
|
||||
}
|
||||
|
||||
/* Selected the highest available SPI clock speed that is still below the MM6108's maximum of
|
||||
* 50MHz */
|
||||
spi_device_interface_config_t dev_cfg = {
|
||||
.clock_speed_hz = SPI_MASTER_FREQ_40M,
|
||||
.mode = 0,
|
||||
.spics_io_num = -1,
|
||||
.queue_size = 1,
|
||||
};
|
||||
ret = spi_bus_add_device(SPI2_HOST, &dev_cfg, &spi_handle);
|
||||
if (ret != ESP_OK) {
|
||||
printf("spi_bus_add_device failed\n");
|
||||
}
|
||||
|
||||
/* The actual clock frequency may not be the one that was set as it is re-calculated by the
|
||||
* driver to the nearest hardware-compatible number. Importantly it is the "nearest", so it could be above
|
||||
* the value set. */
|
||||
int actual_freq_khz = 0;
|
||||
spi_device_get_actual_freq(spi_handle, &actual_freq_khz);
|
||||
printf("Actual SPI CLK %dkHz\n", actual_freq_khz);
|
||||
}
|
||||
|
||||
static void wlan_hal_spi_deinit(void)
|
||||
{
|
||||
esp_err_t ret = spi_bus_remove_device(spi_handle);
|
||||
if (ret != ESP_OK) {
|
||||
printf("spi_bus_remove_device failed\n");
|
||||
}
|
||||
|
||||
ret = spi_bus_free(SPI2_HOST);
|
||||
if (ret != ESP_OK) {
|
||||
printf("spi_bus_initialize failed\n");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Minium transfer length in bytes before interrupt based transactions are used. This is because
|
||||
* there is some setup time associated with using the interrupt based method when compared to the
|
||||
* polling method. In the cases where the difference in setup time exceeds the transaction duration
|
||||
* it is more efficient to uses the polling method instead of the interrupt based one. The below
|
||||
* equation was used to calculate this.
|
||||
*
|
||||
* (DMA_TRANSACTION_DURATION - POLL_TRANSACTION_DURATION) / (8/SPI_FREQ)
|
||||
*
|
||||
* The typical duration for the ESP32 can be found in the [transaction
|
||||
* duration](https://docs.espressif.com/projects/esp-idf/en/v5.1.1/esp32s3/api-reference/peripherals/spi_master.html#transaction-duration)
|
||||
* section of the docs.
|
||||
*/
|
||||
#define INTERRUPT_TRANSFER_MIN_LENGTH 75
|
||||
|
||||
static void spi_master_rw(const uint8_t *w_data, uint8_t *r_data, size_t len)
|
||||
{
|
||||
spi_transaction_t trans_desc = {
|
||||
.rx_buffer = r_data,
|
||||
.tx_buffer = w_data,
|
||||
.length = (len * 8),
|
||||
.flags = 0,
|
||||
};
|
||||
|
||||
esp_err_t err;
|
||||
if (len < INTERRUPT_TRANSFER_MIN_LENGTH) {
|
||||
err = spi_device_polling_transmit(spi_handle, &trans_desc);
|
||||
} else {
|
||||
err = spi_device_transmit(spi_handle, &trans_desc);
|
||||
}
|
||||
|
||||
if (err != ESP_OK) {
|
||||
printf("SPI rw error = %x\n", err);
|
||||
}
|
||||
}
|
||||
|
||||
void mmhal_wlan_hard_reset(void)
|
||||
{
|
||||
gpio_set_level(CONFIG_MM_RESET_N, 0);
|
||||
mmosal_task_sleep(5);
|
||||
gpio_set_level(CONFIG_MM_RESET_N, 1);
|
||||
mmosal_task_sleep(20);
|
||||
}
|
||||
|
||||
void mmhal_wlan_spi_cs_assert(void)
|
||||
{
|
||||
gpio_set_level(CONFIG_MM_SPI_CS, 0);
|
||||
}
|
||||
|
||||
void mmhal_wlan_spi_cs_deassert(void)
|
||||
{
|
||||
gpio_set_level(CONFIG_MM_SPI_CS, 1);
|
||||
}
|
||||
|
||||
uint8_t mmhal_wlan_spi_rw(uint8_t data)
|
||||
{
|
||||
uint8_t readval;
|
||||
spi_master_rw(&data, &readval, 1);
|
||||
return readval;
|
||||
}
|
||||
|
||||
void mmhal_wlan_spi_read_buf(uint8_t *buf, unsigned len)
|
||||
{
|
||||
spi_master_rw(NULL, buf, len);
|
||||
}
|
||||
|
||||
void mmhal_wlan_spi_write_buf(const uint8_t *buf, unsigned len)
|
||||
{
|
||||
spi_master_rw(buf, NULL, len);
|
||||
}
|
||||
|
||||
void mmhal_wlan_send_training_seq(void)
|
||||
{
|
||||
mmhal_wlan_spi_cs_deassert();
|
||||
/* Send >74 clock pulses to card to stabilize CLK.
|
||||
* This method of stacking up the TX data is described in RM0090 rev 19 Figure 253.
|
||||
* It results is a reduction in the time between bytes of ~85% (316ns -> 48ns).
|
||||
* Could not get this to work for the other transactions however.
|
||||
*/
|
||||
uint8_t buf[BYTE_TRAIN] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
spi_master_rw(buf, NULL, BYTE_TRAIN);
|
||||
}
|
||||
|
||||
void mmhal_wlan_register_spi_irq_handler(mmhal_irq_handler_t handler)
|
||||
{
|
||||
spi_irq_handler = handler;
|
||||
gpio_isr_handler_add(CONFIG_MM_SPI_IRQ, (gpio_isr_t)spi_irq_handler, NULL);
|
||||
}
|
||||
|
||||
bool mmhal_wlan_spi_irq_is_asserted(void)
|
||||
{
|
||||
return !gpio_get_level(CONFIG_MM_SPI_IRQ);
|
||||
}
|
||||
|
||||
void mmhal_wlan_set_spi_irq_enabled(bool enabled)
|
||||
{
|
||||
if (enabled) {
|
||||
gpio_set_intr_type(CONFIG_MM_SPI_IRQ, GPIO_INTR_LOW_LEVEL);
|
||||
} else {
|
||||
gpio_set_intr_type(CONFIG_MM_SPI_IRQ, GPIO_INTR_DISABLE);
|
||||
}
|
||||
}
|
||||
|
||||
void mmhal_wlan_init(void)
|
||||
{
|
||||
wlan_hal_gpio_init();
|
||||
wlan_hal_spi_init();
|
||||
/* Raise the RESET_N line to enable the WLAN transceiver. */
|
||||
gpio_set_level(CONFIG_MM_RESET_N, 1);
|
||||
}
|
||||
|
||||
void mmhal_wlan_deinit(void)
|
||||
{
|
||||
/* Lower the RESET_N line to disable the WLAN transceiver. This will put the transceiver in its
|
||||
* lowest power state. */
|
||||
gpio_set_level(CONFIG_MM_RESET_N, 0);
|
||||
|
||||
wlan_hal_spi_deinit();
|
||||
|
||||
/* Clean up any ISR handlers that have been added. These will be added again if the WLAN
|
||||
* interface is brought back up. */
|
||||
gpio_isr_handler_remove(CONFIG_MM_SPI_IRQ);
|
||||
gpio_isr_handler_remove(CONFIG_MM_BUSY);
|
||||
}
|
||||
|
||||
void mmhal_wlan_wake_assert(void)
|
||||
{
|
||||
gpio_set_level(CONFIG_MM_WAKE, 1);
|
||||
}
|
||||
|
||||
void mmhal_wlan_wake_deassert(void)
|
||||
{
|
||||
gpio_set_level(CONFIG_MM_WAKE, 0);
|
||||
}
|
||||
|
||||
bool mmhal_wlan_busy_is_asserted(void)
|
||||
{
|
||||
return gpio_get_level(CONFIG_MM_BUSY);
|
||||
}
|
||||
|
||||
void mmhal_wlan_register_busy_irq_handler(mmhal_irq_handler_t handler)
|
||||
{
|
||||
busy_irq_handler = handler;
|
||||
gpio_isr_handler_add(CONFIG_MM_BUSY, (gpio_isr_t)busy_irq_handler, NULL);
|
||||
}
|
||||
|
||||
void mmhal_wlan_set_busy_irq_enabled(bool enabled)
|
||||
{
|
||||
if (enabled) {
|
||||
gpio_set_intr_type(CONFIG_MM_BUSY, GPIO_INTR_POSEDGE);
|
||||
} else {
|
||||
gpio_set_intr_type(CONFIG_MM_BUSY, GPIO_INTR_DISABLE);
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* USE_MM_IOT_ESP32 */
|
||||
@@ -0,0 +1,35 @@
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Persistent device-log capture (recorder + Datadog cursor).
|
||||
# Cross-session JSONL streams written by the autouse Recorder singleton
|
||||
# (see src/meshtastic_mcp/recorder/). Lives outside tests/ so the pytest
|
||||
# fixture truncate doesn't touch it.
|
||||
.mtlog/
|
||||
|
||||
# Test harness artifacts
|
||||
tests/report.html
|
||||
tests/junit.xml
|
||||
tests/reportlog.jsonl
|
||||
tests/fwlog.jsonl
|
||||
# Subprocess-output tee from pio/esptool/nrfutil/picotool (live flash
|
||||
# progress for the TUI; also a post-run diagnostic for plain CLI runs).
|
||||
tests/flash.log
|
||||
tests/tool_coverage.json
|
||||
tests/.coverage
|
||||
htmlcov/
|
||||
# Persistent run counter for meshtastic-mcp-test-tui header.
|
||||
tests/.tui-runs
|
||||
# Cross-run history (TUI duration sparkline).
|
||||
tests/.history/
|
||||
# Reproducer bundles (TUI `x` export on failed tests).
|
||||
tests/reproducers/
|
||||
# UI-tier camera captures + per-test transcripts. Regenerated every run;
|
||||
# left on disk for human review between runs.
|
||||
tests/ui_captures/
|
||||
@@ -0,0 +1,412 @@
|
||||
# Meshtastic MCP Server
|
||||
|
||||
An [MCP](https://modelcontextprotocol.io) server for working with the Meshtastic firmware repo and connected devices. Lets Claude Code / Claude Desktop:
|
||||
|
||||
- Discover USB-connected Meshtastic devices
|
||||
- Enumerate PlatformIO board variants (166+) with Meshtastic metadata
|
||||
- Build, clean, flash, erase-and-flash (factory), and OTA-update firmware
|
||||
- Read serial logs via `pio device monitor` (with board-specific exception decoders)
|
||||
- Trigger 1200bps touch-reset for bootloader entry (nRF52, ESP32-S3, RP2040)
|
||||
- Query and administer a running node via the [`meshtastic` Python API](https://github.com/meshtastic/python): owner name, config (LocalConfig + ModuleConfig), channels, messaging, reboot/shutdown/factory-reset
|
||||
- Call `esptool`, `nrfutil`, `picotool` directly when PlatformIO doesn't cover the operation
|
||||
|
||||
## Design principle
|
||||
|
||||
**PlatformIO first.** Its `pio run -t upload` knows the correct protocol, offsets, and post-build chain for every variant in `variants/`. Direct vendor-tool wrappers (`esptool_*`, `nrfutil_*`, `picotool_*`) exist as escape hatches for operations pio doesn't cover (blank-chip erase, DFU `.zip` packages, BOOTSEL-mode inspection).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python ≥ 3.11
|
||||
- [PlatformIO Core](https://platformio.org/install/cli) — `pio` on `$PATH` or at `~/.platformio/penv/bin/pio`
|
||||
- The Meshtastic firmware repo checked out somewhere (set via `MESHTASTIC_FIRMWARE_ROOT`)
|
||||
- Optional: `esptool`, `nrfutil`, `picotool` on `$PATH` (or under the firmware venv at `.venv/bin/`) if you want to use the direct-tool wrappers
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
cd <firmware-repo>/mcp-server
|
||||
python3 -m venv .venv
|
||||
.venv/bin/pip install -e .
|
||||
```
|
||||
|
||||
Verify:
|
||||
|
||||
```bash
|
||||
MESHTASTIC_FIRMWARE_ROOT=<firmware-repo> .venv/bin/python -m meshtastic_mcp
|
||||
```
|
||||
|
||||
The server blocks on stdin (that's correct — it speaks MCP over stdio). Ctrl-C to exit.
|
||||
|
||||
## Register with Claude Code
|
||||
|
||||
Edit `~/.claude/settings.json` (global) or `<firmware-repo>/.claude/settings.local.json` (project-only):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"meshtastic": {
|
||||
"command": "<firmware-repo>/mcp-server/.venv/bin/python",
|
||||
"args": ["-m", "meshtastic_mcp"],
|
||||
"env": {
|
||||
"MESHTASTIC_FIRMWARE_ROOT": "<firmware-repo>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Replace `<firmware-repo>` with the absolute path, e.g. `/Users/you/GitHub/firmware`. Restart Claude Code after editing.
|
||||
|
||||
## Register with Claude Desktop
|
||||
|
||||
Same `mcpServers` block, but in `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows).
|
||||
|
||||
## Tools (43)
|
||||
|
||||
### Discovery & metadata
|
||||
|
||||
| Tool | What it does |
|
||||
| -------------- | ------------------------------------------------------------------------------------------ |
|
||||
| `list_devices` | USB/serial port listing, flags likely-Meshtastic candidates |
|
||||
| `list_boards` | PlatformIO envs with `custom_meshtastic_*` metadata; filters by arch/supported/query/level |
|
||||
| `get_board` | Full env dict incl. raw pio config |
|
||||
|
||||
### Build & flash
|
||||
|
||||
| Tool | What it does |
|
||||
| ----------------- | -------------------------------------------------------------------- |
|
||||
| `build` | `pio run -e <env>` (+ mtjson target) |
|
||||
| `clean` | `pio run -e <env> -t clean` |
|
||||
| `pio_flash` | `pio run -e <env> -t upload --upload-port <port>` — any architecture |
|
||||
| `erase_and_flash` | ESP32 full factory flash via `bin/device-install.sh` |
|
||||
| `update_flash` | ESP32 OTA app-partition update via `bin/device-update.sh` |
|
||||
| `touch_1200bps` | 1200-baud open/close to trigger USB CDC bootloader entry |
|
||||
|
||||
### Serial log sessions
|
||||
|
||||
Backed by long-running `pio device monitor` subprocesses with a 10k-line ring buffer per session and board-specific filters (`esp32_exception_decoder` auto-selected when you pass `env=`).
|
||||
|
||||
| Tool | What it does |
|
||||
| -------------- | ------------------------------------------------------------------ |
|
||||
| `serial_open` | Start a monitor session; returns `session_id` |
|
||||
| `serial_read` | Cursor-based pull; reports `dropped` if lines aged out of the ring |
|
||||
| `serial_list` | All active sessions |
|
||||
| `serial_close` | Terminate a session |
|
||||
|
||||
### Device reads
|
||||
|
||||
| Tool | What it does |
|
||||
| ------------- | --------------------------------------------------------------------------- |
|
||||
| `device_info` | my_node_num, long/short name, firmware version, region, channel, node count |
|
||||
| `list_nodes` | Full node database with position, SNR, RSSI, last_heard, battery |
|
||||
|
||||
_The tool tables below document 38 currently registered MCP server tools._
|
||||
|
||||
### Device writes
|
||||
|
||||
| Tool | What it does |
|
||||
| ------------------- | -------------------------------------------------------------------------- |
|
||||
| `set_owner` | Long name + optional short name (≤4 chars) |
|
||||
| `get_config` | One section or all (LocalConfig + ModuleConfig) |
|
||||
| `set_config` | Dot-path field write: `lora.region`=`"US"`, `device.role`=`"ROUTER"`, etc. |
|
||||
| `get_channel_url` | Primary-only or include_all=admin URL |
|
||||
| `set_channel_url` | Import channels from a Meshtastic URL |
|
||||
| `set_debug_log_api` | Enable or disable debug logging for the Meshtastic Python API client |
|
||||
| `send_text` | Broadcast or direct text message |
|
||||
| `reboot` | `localNode.reboot(secs)` — requires `confirm=True` |
|
||||
| `shutdown` | `localNode.shutdown(secs)` — requires `confirm=True` |
|
||||
| `factory_reset` | `localNode.factoryReset(full?)` — requires `confirm=True` |
|
||||
|
||||
### Direct hardware tools (escape hatches)
|
||||
|
||||
| Tool | What it does |
|
||||
| --------------------- | --------------------------------------------------------- |
|
||||
| `esptool_chip_info` | Read chip, MAC, crystal, flash size |
|
||||
| `esptool_erase_flash` | Full-chip erase (destructive) |
|
||||
| `esptool_raw` | Pass-through; confirm=True required for write/erase/merge |
|
||||
| `nrfutil_dfu` | DFU-flash a `.zip` package |
|
||||
| `nrfutil_raw` | Pass-through |
|
||||
| `picotool_info` | Read Pico BOOTSEL-mode info |
|
||||
| `picotool_load` | Load a UF2 |
|
||||
| `picotool_raw` | Pass-through |
|
||||
|
||||
### USB power control (uhubctl)
|
||||
|
||||
| Tool | What it does |
|
||||
| --------------- | ----------------------------------------------------------- |
|
||||
| `uhubctl_list` | Enumerate USB hubs + attached-device VID/PID (read-only) |
|
||||
| `uhubctl_power` | Drive a hub port `on` or `off`; `off` requires confirm=True |
|
||||
| `uhubctl_cycle` | Off → wait `delay_s` → on; confirm=True required |
|
||||
|
||||
Target a port by explicit `(location, port)` (raw uhubctl syntax like
|
||||
`location="1-1.3", port=2`) or by `role` (`"nrf52"`, `"esp32s3"`). Role
|
||||
lookup checks `MESHTASTIC_UHUBCTL_LOCATION_<ROLE>` +
|
||||
`MESHTASTIC_UHUBCTL_PORT_<ROLE>` env vars first, then auto-detects via VID
|
||||
against `uhubctl`'s output.
|
||||
|
||||
Requires [`uhubctl`](https://github.com/mvp/uhubctl) on PATH:
|
||||
|
||||
```bash
|
||||
brew install uhubctl # macOS
|
||||
apt install uhubctl # Debian/Ubuntu
|
||||
```
|
||||
|
||||
Modern macOS + PPPS-capable hubs generally work without root. On Linux
|
||||
without udev rules, or on old macOS with driver quirks, you may need
|
||||
`sudo`. If uhubctl returns a permission error the MCP tool raises a
|
||||
clear `UhubctlError` pointing at the
|
||||
[udev-rules / sudo fallback](https://github.com/mvp/uhubctl#linux-usb-permissions)
|
||||
rather than auto-`sudo`'ing mid-run.
|
||||
|
||||
## Safety
|
||||
|
||||
- **All destructive flash/admin tools require `confirm=True`** as a tool-level gate, on top of any permission prompt from Claude.
|
||||
- **Serial port is exclusive.** If a `serial_*` session is active on a port, `device_info`/admin tools on the same port will fail fast with a pointer at the active `session_id`. Close the session first.
|
||||
- **Flash confirmation by architecture**: `erase_and_flash` / `update_flash` error if the env's architecture isn't ESP32 — use `pio_flash` for nRF52/RP2040/STM32.
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Var | Default | Purpose |
|
||||
| -------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
||||
| `MESHTASTIC_FIRMWARE_ROOT` | walks up from cwd for `platformio.ini` | Pin the firmware repo |
|
||||
| `MESHTASTIC_PIO_BIN` | `~/.platformio/penv/bin/pio` → `$PATH` `pio` → `platformio` | Override `pio` location |
|
||||
| `MESHTASTIC_ESPTOOL_BIN` | `<firmware>/.venv/bin/esptool` → `$PATH` | Override esptool |
|
||||
| `MESHTASTIC_NRFUTIL_BIN` | `$PATH` | Override nrfutil |
|
||||
| `MESHTASTIC_PICOTOOL_BIN` | `$PATH` | Override picotool |
|
||||
| `MESHTASTIC_MCP_SEED` | `mcp-<user>-<host>` | PSK seed for test-harness session (CI override) |
|
||||
| `MESHTASTIC_MCP_FLASH_LOG` | `<mcp-server>/tests/flash.log` | Tee target for pio/esptool/nrfutil subprocess output (TUI tails it) |
|
||||
| `MESHTASTIC_MCP_TCP_HOST` | unset | `host` or `host:port` of a `meshtasticd` daemon to surface as a TCP device (see "TCP / native-host nodes" below) |
|
||||
|
||||
## TCP / native-host nodes
|
||||
|
||||
The `native-macos` and `native` PlatformIO envs build a headless `meshtasticd`
|
||||
binary that runs on the host (Apple Silicon / Intel macOS, or Linux Portduino).
|
||||
The daemon exposes the meshtastic TCP API on port `4403` rather than a USB
|
||||
serial endpoint — point the MCP server at it via `MESHTASTIC_MCP_TCP_HOST`:
|
||||
|
||||
```bash
|
||||
# 1. Build + run a daemon on this host (see variants/native/portduino/platformio.ini
|
||||
# for full Homebrew prereqs and CH341 LoRa-adapter setup).
|
||||
pio run -e native-macos
|
||||
~/.meshtasticd/meshtasticd
|
||||
|
||||
# 2. Point the MCP server at it.
|
||||
export MESHTASTIC_MCP_TCP_HOST=localhost # or host:port, default port 4403
|
||||
```
|
||||
|
||||
**First-run gotcha — MAC address.** `meshtasticd` derives its MAC from the
|
||||
USB adapter's serial-number / product strings. Many cheap CH341 dongles
|
||||
(MeshStick included — VID 0x1A86 / PID 0x5512) ship with `iSerialNumber=0`
|
||||
and `iProduct=0`, so the daemon aborts on boot with `*** Blank MAC Address
|
||||
not allowed!`. Set the MAC explicitly in `config.yaml`:
|
||||
|
||||
```yaml
|
||||
# Under General:
|
||||
MACAddress: 02:CA:FE:BA:BE:01
|
||||
```
|
||||
|
||||
Use a locally-administered address (first byte's second-LSB set, e.g.
|
||||
`02:*` / `06:*` / `0A:*` / `0E:*`) to avoid colliding with a real OUI.
|
||||
|
||||
There is also a `--hwid AA:BB:CC:DD:EE:FF` CLI flag visible in
|
||||
`meshtasticd --help`, but it is **currently broken** in
|
||||
`MAC_from_string()` (`src/platform/portduino/PortduinoGlue.cpp`): the
|
||||
function strips colons from its parameter but then reads bytes from the
|
||||
global `portduino_config.mac_address`, so `--hwid` is silently overridden
|
||||
when `MACAddress:` is also set, and crashes the daemon (uncaught
|
||||
`std::invalid_argument: stoi: no conversion`) when it isn't. Use the YAML
|
||||
form until that's fixed upstream.
|
||||
|
||||
`list_devices` will surface the daemon as `tcp://localhost:4403` with
|
||||
`likely_meshtastic=True`, so `device_info`, `list_nodes`, `get_config`,
|
||||
`set_config`, `set_owner`, `send_text`, `userprefs_*`, and the admin RPCs
|
||||
auto-select it when no `port` is passed. Pass `port="tcp://other-host:9999"`
|
||||
explicitly to target a different daemon.
|
||||
|
||||
**Tools that don't apply to a TCP/native node** (no USB hardware to operate
|
||||
on) raise a clear `ConnectionError` rather than failing mysteriously:
|
||||
`pio_flash`, `erase_and_flash`, `update_flash`, `touch_1200bps`,
|
||||
`serial_open` (use info/admin tools directly), and the vendor escape hatches
|
||||
`esptool_*`, `nrfutil_*`, `picotool_*`. `pio_flash` against a `native*` env
|
||||
similarly raises — there's no upload step; use `build` and run the binary
|
||||
directly.
|
||||
|
||||
The pytest harness in `tests/` still assumes USB-attached devices per role —
|
||||
TCP-aware fixtures are not part of this surface yet.
|
||||
|
||||
## Hardware Test Suite
|
||||
|
||||
`mcp-server/tests/` holds a pytest-based integration suite that exercises
|
||||
real USB-connected Meshtastic devices against the MCP server surface. Separate
|
||||
from the native C++ unit tests in the firmware repo's top-level `test/`
|
||||
directory — this one validates the device-facing behavior end-to-end.
|
||||
|
||||
### Invocation
|
||||
|
||||
```bash
|
||||
./mcp-server/run-tests.sh # full suite (auto-detect + auto-bake-if-needed)
|
||||
./mcp-server/run-tests.sh --force-bake # reflash devices before testing
|
||||
./mcp-server/run-tests.sh --assume-baked # skip the bake step (caller vouches for state)
|
||||
./mcp-server/run-tests.sh tests/mesh # one tier
|
||||
./mcp-server/run-tests.sh tests/mesh/test_traceroute.py # one file
|
||||
./mcp-server/run-tests.sh -k telemetry # pytest name filter
|
||||
```
|
||||
|
||||
The wrapper auto-detects connected devices (VID `0x239A` → `nrf52` → env
|
||||
`rak4631`; `0x303A` or `0x10C4` → `esp32s3` → env `heltec-v3`), exports
|
||||
`MESHTASTIC_MCP_ENV_<ROLE>` env vars, and invokes pytest. Overrides via
|
||||
per-role env vars: `MESHTASTIC_MCP_ENV_NRF52=heltec-mesh-node-t114 ./run-tests.sh`.
|
||||
|
||||
No hardware connected? The wrapper narrows to `tests/unit/` only and says so
|
||||
in the pre-flight header.
|
||||
|
||||
### Tiers (run in this order)
|
||||
|
||||
- **`bake`** (`tests/test_00_bake.py`) — flashes both hub roles with the
|
||||
session's test profile. Has a skip-if-already-baked check (region + channel
|
||||
match); `--force-bake` overrides.
|
||||
- **`unit`** — pure Python, no hardware. boards / PIO wrapper /
|
||||
userPrefs-parse / testing-profile fixtures.
|
||||
- **`mesh`** — 2-device mesh: formation, broadcast delivery, direct+ACK,
|
||||
traceroute, bidirectional. Parametrized over both directions. Includes
|
||||
`test_peer_offline_recovery` which uses uhubctl to power-cycle one peer
|
||||
mid-conversation and verifies the mesh recovers (skips without uhubctl).
|
||||
- **`telemetry`** — periodic telemetry broadcast + on-demand request/reply
|
||||
(`TELEMETRY_APP` with `wantResponse=True`).
|
||||
- **`monitor`** — boot log has no panic markers within 60 s of reboot.
|
||||
- **`recovery`** — `uhubctl` power-cycle round-trip: verifies the hub port
|
||||
can be toggled off/on, the device re-enumerates with the same
|
||||
`my_node_num`, and NVS-resident config (region, channel, modem preset)
|
||||
survives a hard reset. Requires `uhubctl` on PATH; skips cleanly otherwise.
|
||||
- **`ui`** — input-broker-driven screen navigation (`AdminMessage.send_input_event`
|
||||
injection → `Screen::handleInputEvent` → frame transition). Parametrized
|
||||
on the screen-bearing role (heltec-v3 OLED). Captures images via USB
|
||||
webcam + OCRs them for HTML-report evidence. Requires `pip install -e '.[ui]'`
|
||||
and `MESHTASTIC_UI_CAMERA_DEVICE_ESP32S3=<index>`; tier is auto-deselected
|
||||
if `cv2` isn't importable.
|
||||
- **`fleet`** — PSK-seed isolation: two labs with different seeds never
|
||||
overlap.
|
||||
- **`admin`** — owner persistence across reboot, channel URL round-trip,
|
||||
`lora.hop_limit` persistence.
|
||||
- **`provisioning`** — region/channel baking, userPrefs survive
|
||||
`factory_reset(full=False)`.
|
||||
|
||||
#### UI tier setup
|
||||
|
||||
The `tests/ui/` tier drives the on-device OLED via the firmware's existing
|
||||
`AdminMessage.send_input_event` RPC (no firmware changes required) and
|
||||
verifies transitions via a macro-gated log line + camera + OCR. Summary:
|
||||
|
||||
1. Install extras: `pip install -e 'mcp-server/.[ui]'` — pulls in
|
||||
`opencv-python-headless`, `numpy`, `easyocr`, `Pillow`. First easyocr
|
||||
run downloads ~100 MB of models to `~/.EasyOCR/`; an autouse session
|
||||
fixture pre-warms the reader so per-test OCR is <100 ms after that.
|
||||
2. Point a USB webcam at the heltec-v3 OLED. Discover its index:
|
||||
```bash
|
||||
.venv/bin/python -c "import cv2; [print(i, cv2.VideoCapture(i).read()[0]) for i in range(5)]"
|
||||
```
|
||||
3. Export the per-role device env var:
|
||||
```bash
|
||||
export MESHTASTIC_UI_CAMERA_DEVICE_ESP32S3=0
|
||||
```
|
||||
4. Run:
|
||||
```bash
|
||||
./run-tests.sh tests/ui -v
|
||||
```
|
||||
Captures land under `tests/ui_captures/<session_seed>/<test_id>/`, one
|
||||
PNG + `.ocr.txt` per `frame_capture()` call, with a per-test
|
||||
`transcript.md` stepping through event → frame → OCR. The HTML report
|
||||
embeds the full image strip inline (pass or fail).
|
||||
|
||||
On macOS, `cv2.VideoCapture(0)` triggers the TCC Camera permission prompt
|
||||
on first use. Pre-grant Terminal (or your IDE's terminal) before running.
|
||||
The `OpenCVBackend` fails fast on 10 consecutive black frames so a silent
|
||||
permission denial surfaces as a clear error, not an empty PNG strip.
|
||||
|
||||
No camera? Set `MESHTASTIC_UI_CAMERA_BACKEND=null` (or leave the device var
|
||||
unset). Tests still exercise the event-injection path and log assertions;
|
||||
captures just become 1×1 black PNGs.
|
||||
|
||||
### Artifacts (regenerated every run, under `tests/`)
|
||||
|
||||
- `report.html` — self-contained pytest-html report. Each test gets a
|
||||
**Meshtastic debug** section attached on failure with a 200-line firmware
|
||||
log tail + device-state dump. Open this first on failures.
|
||||
- `junit.xml` — CI-parseable.
|
||||
- `reportlog.jsonl` — `pytest-reportlog` event stream; consumed by the TUI.
|
||||
- `fwlog.jsonl` — firmware log mirror (`meshtastic.log.line` pubsub → JSONL).
|
||||
- `flash.log` — tee of all pio / esptool / nrfutil / picotool subprocess
|
||||
output during the run (driven by `MESHTASTIC_MCP_FLASH_LOG`).
|
||||
|
||||
### Live TUI
|
||||
|
||||
```bash
|
||||
.venv/bin/meshtastic-mcp-test-tui
|
||||
.venv/bin/meshtastic-mcp-test-tui tests/mesh # pytest args pass through
|
||||
```
|
||||
|
||||
Textual-based wrapper over `run-tests.sh` with a live test tree, tier
|
||||
counters, pytest output pane, firmware-log pane, and a device-status strip.
|
||||
Key bindings: `r` re-run focused, `f` filter, `d` failure detail, `g` open
|
||||
`report.html`, `x` export reproducer bundle, `l` cycle fw-log filter, `q`
|
||||
quit (SIGINT → SIGTERM → SIGKILL escalation).
|
||||
|
||||
Set `MESHTASTIC_UI_TUI_CAMERA=1` to mount a bottom-of-screen **UI camera**
|
||||
panel. Left side: the latest capture PNG rendered as Unicode half-blocks
|
||||
(via `rich-pixels`, works in any terminal — no kitty/sixel required).
|
||||
Right side: live transcript tail ("step 3 — frame 4/8 name=nodelist_nodes
|
||||
— OCR: Nodes 2/2") so you can see every event-injection and its result
|
||||
as each UI test runs. Requires the `[ui]` extras for image rendering; the
|
||||
transcript alone works without them.
|
||||
|
||||
### Slash commands
|
||||
|
||||
Three AI-assisted workflows are wired up for Claude Code operators
|
||||
(`.claude/commands/`) and Copilot operators (`.github/prompts/`):
|
||||
`/test` (run + interpret), `/diagnose` (read-only health report), `/repro`
|
||||
(flake triage, N-times re-run with log diff).
|
||||
|
||||
### House rules (for human + agent contributors)
|
||||
|
||||
- Session-scoped fixtures in `tests/conftest.py` snapshot + restore
|
||||
`userPrefs.jsonc`; **never edit `userPrefs.jsonc` from inside a test**.
|
||||
Use the `test_profile` / `no_region_profile` fixtures for ephemeral
|
||||
overrides.
|
||||
- `SerialInterface` holds an **exclusive port lock**; sequence calls
|
||||
open → mutate → close, then next device. No parallel calls to the
|
||||
same port.
|
||||
- Directed PKI-encrypted sends need **bilateral NodeInfo warmup** —
|
||||
both sides must hold the other's current pubkey. See
|
||||
`tests/mesh/_receive.py::nudge_nodeinfo_port` and the three directed-
|
||||
send tests (`test_direct_with_ack`, `test_traceroute`,
|
||||
`test_telemetry_request_reply`) for the canonical pattern.
|
||||
|
||||
## Layout
|
||||
|
||||
```text
|
||||
mcp-server/
|
||||
├── pyproject.toml
|
||||
├── README.md
|
||||
└── src/meshtastic_mcp/
|
||||
├── __main__.py # entry: python -m meshtastic_mcp
|
||||
├── server.py # FastMCP app + @app.tool() registrations (thin)
|
||||
├── config.py # firmware_root, pio_bin, esptool_bin, etc.
|
||||
├── pio.py # subprocess wrapper (timeouts, JSON, tail_lines)
|
||||
├── devices.py # list_devices (findPorts + comports)
|
||||
├── boards.py # list_boards / get_board (pio project config parse + cache)
|
||||
├── flash.py # build, clean, flash, erase_and_flash, update_flash, touch_1200bps
|
||||
├── serial_session.py # SerialSession + reader thread + ring buffer
|
||||
├── registry.py # session registry + per-port locks
|
||||
├── connection.py # connect(port) ctx mgr — SerialInterface + port lock
|
||||
├── info.py # device_info, list_nodes
|
||||
├── admin.py # set_owner, get/set_config, channels, send_text, reboot/shutdown/factory_reset
|
||||
└── hw_tools.py # esptool / nrfutil / picotool wrappers
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **"Could not locate Meshtastic firmware root"** — set `MESHTASTIC_FIRMWARE_ROOT`.
|
||||
- **"Could not find `pio`"** — install PlatformIO or set `MESHTASTIC_PIO_BIN`.
|
||||
- **"Port is held by serial session ..."** — call `serial_close(session_id)` or `serial_list` to find it.
|
||||
- **`factory.bin` not found after build** — the env may not be ESP32; only ESP32 envs produce a `.factory.bin`.
|
||||
- **`touch_1200bps` reported `new_port: null`** — the device may not have 1200bps-reset stdio, or the bootloader re-uses the same port name. Check `list_devices` manually.
|
||||
@@ -0,0 +1,54 @@
|
||||
[project]
|
||||
name = "meshtastic-mcp"
|
||||
version = "0.1.0"
|
||||
description = "MCP server for Meshtastic firmware development: device discovery, PlatformIO tooling, flashing, serial monitoring, and device administration via the meshtastic Python API."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
license = { text = "GPL-3.0-only" }
|
||||
authors = [{ name = "thebentern" }]
|
||||
dependencies = ["mcp>=1.2", "pyserial>=3.5", "meshtastic>=2.7.8"]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = ["pytest>=7"]
|
||||
test = [
|
||||
"pytest>=8",
|
||||
"pytest-html>=4",
|
||||
"pytest-reportlog>=0.4",
|
||||
"pytest-timeout>=2.3",
|
||||
"coverage[toml]>=7",
|
||||
"pyyaml>=6",
|
||||
# textual is required by the `meshtastic-mcp-test-tui` script (see
|
||||
# `src/meshtastic_mcp/cli/test_tui.py`). Bundled into `test` rather than a
|
||||
# separate `[tui]` extra because v1 expects test operators are the only
|
||||
# consumers; revisit if install cost pushes back.
|
||||
"textual>=0.50",
|
||||
]
|
||||
# UI test tier + `capture_screen` MCP tool. Optional because the ML OCR
|
||||
# model alone is ~100 MB and camera hardware is user-supplied.
|
||||
# pip install -e '.[ui]' — full (OpenCV + easyocr)
|
||||
# pip install -e '.[ui-min]' — image capture only, no OCR
|
||||
ui = [
|
||||
"opencv-python-headless>=4.9",
|
||||
"numpy>=1.26",
|
||||
"easyocr>=1.7",
|
||||
"Pillow>=10.0",
|
||||
# Renders the latest camera capture as Unicode half-blocks in the TUI
|
||||
# (MESHTASTIC_UI_TUI_CAMERA=1). Terminal-agnostic — no kitty / sixel
|
||||
# dependency. Pure Python, tiny.
|
||||
"rich-pixels>=3.0",
|
||||
]
|
||||
ui-min = ["opencv-python-headless>=4.9", "numpy>=1.26"]
|
||||
|
||||
[project.scripts]
|
||||
meshtastic-mcp = "meshtastic_mcp.__main__:main"
|
||||
# Live TUI wrapping run-tests.sh — shells out to the same script the plain
|
||||
# CLI uses, tails pytest-reportlog for per-test state, and polls the device
|
||||
# list at startup + post-run (port lock forces it to stay idle during the run).
|
||||
meshtastic-mcp-test-tui = "meshtastic_mcp.cli.test_tui:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/meshtastic_mcp"]
|
||||
Executable
+270
@@ -0,0 +1,270 @@
|
||||
#!/usr/bin/env bash
|
||||
# mcp-server hardware test runner.
|
||||
#
|
||||
# Auto-detects connected Meshtastic devices, maps each to its PlatformIO env
|
||||
# via the same role table the pytest fixtures use, exports the right
|
||||
# MESHTASTIC_MCP_ENV_* env vars, and invokes pytest.
|
||||
#
|
||||
# Usage:
|
||||
# ./run-tests.sh # full suite, default pytest args
|
||||
# ./run-tests.sh tests/mesh # subset (any pytest args pass through)
|
||||
# ./run-tests.sh --force-bake # override one default with another
|
||||
# MESHTASTIC_MCP_ENV_NRF52=foo ./run-tests.sh # override env per role
|
||||
# MESHTASTIC_MCP_SEED=ci-run-42 ./run-tests.sh # override PSK seed
|
||||
#
|
||||
# If zero supported devices are detected, only the unit tier runs.
|
||||
#
|
||||
# Also restores `userPrefs.jsonc` from the session-backup sidecar if a prior
|
||||
# run exited abnormally (belt to conftest.py's atexit suspenders).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# cd to the script's directory so relative paths resolve consistently no
|
||||
# matter where the user invoked from.
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
VENV_PY="$SCRIPT_DIR/.venv/bin/python"
|
||||
if [[ ! -x $VENV_PY ]]; then
|
||||
echo "error: $VENV_PY not found or not executable." >&2
|
||||
echo " Bootstrap the venv first:" >&2
|
||||
echo " cd $SCRIPT_DIR && python3 -m venv .venv && .venv/bin/pip install -e '.[test]'" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Resolve firmware root the same way conftest.py does (this script sits in
|
||||
# mcp-server/, firmware repo root is one level up).
|
||||
FIRMWARE_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
USERPREFS_PATH="$FIRMWARE_ROOT/userPrefs.jsonc"
|
||||
USERPREFS_SIDECAR="$USERPREFS_PATH.mcp-session-bak"
|
||||
|
||||
# ---------- Pre-flight: recover stale userPrefs.jsonc from prior crash ----
|
||||
# If conftest.py's atexit hook didn't fire (SIGKILL, kernel panic, OS
|
||||
# restart), the sidecar is the ground truth. Self-heal before running so we
|
||||
# don't bake the previous run's dirty state into this run's firmware.
|
||||
if [[ -f $USERPREFS_SIDECAR ]]; then
|
||||
echo "[pre-flight] found $USERPREFS_SIDECAR from a prior abnormal exit;" >&2
|
||||
echo " restoring userPrefs.jsonc before starting." >&2
|
||||
cp "$USERPREFS_SIDECAR" "$USERPREFS_PATH"
|
||||
rm -f "$USERPREFS_SIDECAR"
|
||||
fi
|
||||
|
||||
# If userPrefs.jsonc has uncommitted changes BEFORE the run starts, that's
|
||||
# worth warning about — tests will snapshot this dirty state and restore to
|
||||
# it at the end, which may not be what the operator wants.
|
||||
if command -v git >/dev/null 2>&1; then
|
||||
cd "$FIRMWARE_ROOT"
|
||||
# Capture the git status into a local first — SC2312 flags command
|
||||
# substitution inside `[[ -n ... ]]` because the exit code of `git
|
||||
# status` is masked. A two-step assignment makes the failure path
|
||||
# explicit (non-git, missing file) and keeps the bracket test clean.
|
||||
_git_status_porcelain="$(git status --porcelain userPrefs.jsonc 2>/dev/null || true)"
|
||||
if [[ -n $_git_status_porcelain ]]; then
|
||||
echo "[pre-flight] warning: userPrefs.jsonc has uncommitted changes." >&2
|
||||
echo " Tests will snapshot THIS state and restore to it" >&2
|
||||
echo " at teardown. If that's not intended, run:" >&2
|
||||
echo " git checkout userPrefs.jsonc" >&2
|
||||
echo " and re-invoke." >&2
|
||||
fi
|
||||
cd "$SCRIPT_DIR"
|
||||
fi
|
||||
|
||||
# ---------- Seed default --------------------------------------------------
|
||||
# Per-machine default so repeated runs from the same operator land on the
|
||||
# same PSK (makes --assume-baked valid across invocations). Operator can
|
||||
# override with an explicit env var if they want isolation (e.g. CI).
|
||||
if [[ -z ${MESHTASTIC_MCP_SEED-} ]]; then
|
||||
WHO="$(whoami 2>/dev/null || echo anon)"
|
||||
HOST="$(hostname -s 2>/dev/null || echo host)"
|
||||
export MESHTASTIC_MCP_SEED="mcp-${WHO}-${HOST}"
|
||||
fi
|
||||
|
||||
# ---------- Flash progress log --------------------------------------------
|
||||
# pio.py / hw_tools.py tee subprocess output (pio run -t upload, esptool,
|
||||
# nrfutil, picotool) to this file line-by-line as it arrives when this env
|
||||
# var is set. The TUI tails it so the operator sees live flash progress
|
||||
# instead of 3 minutes of silence during `test_00_bake.py`. Plain CLI users
|
||||
# also benefit — the log is a post-run diagnostic even without the TUI.
|
||||
# Truncate at session start so each run gets a clean log.
|
||||
export MESHTASTIC_MCP_FLASH_LOG="$SCRIPT_DIR/tests/flash.log"
|
||||
: >"$MESHTASTIC_MCP_FLASH_LOG"
|
||||
|
||||
# ---------- Detect connected hardware -------------------------------------
|
||||
# In-process call to the same Python API the test fixtures use, so the
|
||||
# script never drifts from what pytest sees. Returns a JSON object
|
||||
# {role: port, ...}.
|
||||
ROLES_JSON="$(
|
||||
"$VENV_PY" - <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, "src")
|
||||
from meshtastic_mcp import devices
|
||||
|
||||
# Role → canonical VID map. Kept in sync with
|
||||
# `tests/conftest.py::hub_profile` defaults; if that changes, this must too.
|
||||
ROLE_BY_VID = {
|
||||
0x239A: "nrf52", # Adafruit / RAK nRF52 native USB (app + DFU)
|
||||
0x303A: "esp32s3", # Espressif native USB (ESP32-S3)
|
||||
0x10C4: "esp32s3", # CP2102 USB-UART (common on Heltec/LilyGO ESP32 boards)
|
||||
}
|
||||
|
||||
out: dict[str, str] = {}
|
||||
for dev in devices.list_devices(include_unknown=True):
|
||||
vid_raw = dev.get("vid") or ""
|
||||
try:
|
||||
if isinstance(vid_raw, str) and vid_raw.startswith("0x"):
|
||||
vid = int(vid_raw, 16)
|
||||
else:
|
||||
vid = int(vid_raw)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
role = ROLE_BY_VID.get(vid)
|
||||
# First port wins per role — matches hub_devices fixture semantics.
|
||||
if role and role not in out:
|
||||
out[role] = dev["port"]
|
||||
|
||||
json.dump(out, sys.stdout)
|
||||
PY
|
||||
)"
|
||||
|
||||
# ---------- Map role → pio env --------------------------------------------
|
||||
# Honor MESHTASTIC_MCP_ENV_<ROLE> operator overrides; fall back to the
|
||||
# same defaults hardcoded in tests/conftest.py::_DEFAULT_ROLE_ENVS.
|
||||
resolve_env() {
|
||||
local role="$1"
|
||||
local default="$2"
|
||||
local upper
|
||||
upper="$(echo "$role" | tr '[:lower:]' '[:upper:]')"
|
||||
local var="MESHTASTIC_MCP_ENV_${upper}"
|
||||
eval "local override=\${$var:-}"
|
||||
if [[ -n $override ]]; then
|
||||
echo "$override"
|
||||
else
|
||||
echo "$default"
|
||||
fi
|
||||
}
|
||||
|
||||
NRF52_PORT="$(echo "$ROLES_JSON" | "$VENV_PY" -c 'import json,sys; print(json.loads(sys.stdin.read()).get("nrf52", ""))')"
|
||||
ESP32S3_PORT="$(echo "$ROLES_JSON" | "$VENV_PY" -c 'import json,sys; print(json.loads(sys.stdin.read()).get("esp32s3", ""))')"
|
||||
|
||||
DETECTED=""
|
||||
if [[ -n $NRF52_PORT ]]; then
|
||||
NRF52_ENV="$(resolve_env nrf52 rak4631)"
|
||||
export MESHTASTIC_MCP_ENV_NRF52="$NRF52_ENV"
|
||||
DETECTED="${DETECTED} nrf52 @ ${NRF52_PORT} -> env=${NRF52_ENV}\n"
|
||||
fi
|
||||
if [[ -n $ESP32S3_PORT ]]; then
|
||||
ESP32S3_ENV="$(resolve_env esp32s3 heltec-v3)"
|
||||
export MESHTASTIC_MCP_ENV_ESP32S3="$ESP32S3_ENV"
|
||||
DETECTED="${DETECTED} esp32s3 @ ${ESP32S3_PORT} -> env=${ESP32S3_ENV}\n"
|
||||
fi
|
||||
|
||||
# ---------- Pre-flight summary --------------------------------------------
|
||||
# Surface what pytest is about to do with respect to the bake phase: the
|
||||
# operator should see "will verify + bake if needed" by default, so a
|
||||
# 3-minute flash appearing mid-run isn't a surprise. Detection of the
|
||||
# explicit overrides is best-effort — we just scan $@ for the known flags.
|
||||
_bake_mode="auto (verify + bake if needed)"
|
||||
for _arg in "$@"; do
|
||||
case "$_arg" in
|
||||
--assume-baked) _bake_mode="skip (--assume-baked)" ;;
|
||||
--force-bake) _bake_mode="force (--force-bake)" ;;
|
||||
*) ;; # any other arg: pass-through; bake mode unchanged
|
||||
esac
|
||||
done
|
||||
|
||||
echo "mcp-server test runner"
|
||||
echo " firmware root : $FIRMWARE_ROOT"
|
||||
echo " seed : $MESHTASTIC_MCP_SEED"
|
||||
echo " bake : $_bake_mode"
|
||||
if [[ -n $DETECTED ]]; then
|
||||
echo " detected hub :"
|
||||
printf "%b" "$DETECTED"
|
||||
else
|
||||
echo " detected hub : (none)"
|
||||
fi
|
||||
echo
|
||||
|
||||
# ---------- Invoke pytest -------------------------------------------------
|
||||
# If no devices detected, only the unit tier would produce meaningful
|
||||
# PASS/FAIL — every hardware test would SKIP with "role not present". We
|
||||
# narrow to tests/unit explicitly so the summary reads as "no hardware,
|
||||
# unit suite only" instead of "big skip count looks suspicious".
|
||||
if [[ -z $DETECTED && $# -eq 0 ]]; then
|
||||
echo "[pre-flight] no supported devices detected; running unit tier only."
|
||||
echo
|
||||
exec "$VENV_PY" -m pytest tests/unit -v --report-log=tests/reportlog.jsonl
|
||||
fi
|
||||
|
||||
# Default pytest args when the user passed none. Power users can invoke
|
||||
# `./run-tests.sh tests/mesh -v --tb=long` and skip all of these defaults.
|
||||
#
|
||||
# NOTE: `--assume-baked` is DELIBERATELY omitted here. `tests/test_00_bake.py`
|
||||
# has an internal skip-if-already-baked check (`_bake_role`: query device_info,
|
||||
# compare region + primary_channel to the session profile, skip on match).
|
||||
# So the fast path is ~8-10 s of verification overhead when the devices are
|
||||
# already baked — negligible next to the 2-6 min suite runtime. Letting
|
||||
# test_00_bake.py run means a fresh device, a re-seeded session, or a post-
|
||||
# factory-reset device gets flashed automatically instead of silently
|
||||
# skipping half the hardware tests with "not baked with session profile"
|
||||
# errors. Power users who know their hardware is current and want to shave
|
||||
# those seconds can pass `--assume-baked` explicitly.
|
||||
if [[ $# -eq 0 ]]; then
|
||||
set -- tests/ \
|
||||
--html=tests/report.html --self-contained-html \
|
||||
--junitxml=tests/junit.xml \
|
||||
-v --tb=short
|
||||
fi
|
||||
|
||||
# UI tier requires opencv-python-headless (and ideally easyocr). If it's
|
||||
# not installed, auto-deselect tests/ui so operators without the [ui]
|
||||
# extra still get a green run. Printed in yellow; silent when cv2 is
|
||||
# present.
|
||||
_cv2_ok=0
|
||||
if "$VENV_PY" -c "import cv2" >/dev/null 2>&1; then
|
||||
_cv2_ok=1
|
||||
fi
|
||||
_running_ui=0
|
||||
for _arg in "$@"; do
|
||||
case "$_arg" in
|
||||
*tests/ui* | tests/) _running_ui=1 ;;
|
||||
*) ;;
|
||||
esac
|
||||
done
|
||||
if [[ $_running_ui -eq 1 && $_cv2_ok -eq 0 ]]; then
|
||||
printf '\033[33m[pre-flight] tests/ui tier detected, but opencv-python-headless is not installed — deselecting.\033[0m\n'
|
||||
printf ' install with: .venv/bin/pip install -e "mcp-server/.[ui]"\n'
|
||||
echo
|
||||
set -- "$@" --ignore=tests/ui
|
||||
fi
|
||||
|
||||
# Recovery tier needs `uhubctl` on PATH — it power-cycles devices via USB
|
||||
# hub PPPS. The tier's conftest already skips cleanly, so this is just a
|
||||
# friendly heads-up before the skip happens. `baked_single`'s auto-
|
||||
# recovery hook also benefits from having uhubctl available across the
|
||||
# whole suite.
|
||||
if ! command -v uhubctl >/dev/null 2>&1; then
|
||||
printf "\033[33m[pre-flight] uhubctl not found on PATH — recovery tier will skip, and\n"
|
||||
printf " wedged-device auto-recovery is disabled.\033[0m\n"
|
||||
printf " install with: brew install uhubctl (macOS) or apt install uhubctl (Debian/Ubuntu).\n"
|
||||
echo
|
||||
fi
|
||||
|
||||
# Always emit `tests/reportlog.jsonl` (unless the operator explicitly passed
|
||||
# their own `--report-log=...`). Consumers — notably the
|
||||
# `meshtastic-mcp-test-tui` TUI — tail the reportlog for live per-test state.
|
||||
# Appending here means power-user invocations like `./run-tests.sh tests/mesh`
|
||||
# also produce it, not just the all-defaults invocation.
|
||||
_has_report_log=0
|
||||
for _arg in "$@"; do
|
||||
case "$_arg" in
|
||||
--report-log | --report-log=*) _has_report_log=1 ;;
|
||||
*) ;; # any other arg: no-op; loop continues
|
||||
esac
|
||||
done
|
||||
if [[ $_has_report_log -eq 0 ]]; then
|
||||
set -- "$@" --report-log=tests/reportlog.jsonl
|
||||
fi
|
||||
|
||||
exec "$VENV_PY" -m pytest "$@"
|
||||
@@ -0,0 +1,217 @@
|
||||
{
|
||||
"title": "Meshtastic Firmware — Recorder Stream",
|
||||
"description": "Live view of `.mtlog/` streams shipped by `mtlog_to_datadog.py`. Heap, packet volume, log levels, errors. One row per port.",
|
||||
"widgets": [
|
||||
{
|
||||
"definition": {
|
||||
"title": "Free heap (bytes)",
|
||||
"type": "timeseries",
|
||||
"show_legend": true,
|
||||
"requests": [
|
||||
{
|
||||
"queries": [
|
||||
{
|
||||
"name": "free_heap",
|
||||
"data_source": "metrics",
|
||||
"query": "avg:mesh.local.heap_free_bytes{service:meshtastic-firmware} by {port}"
|
||||
}
|
||||
],
|
||||
"response_format": "timeseries",
|
||||
"display_type": "line"
|
||||
}
|
||||
],
|
||||
"yaxis": { "label": "bytes" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"definition": {
|
||||
"title": "Heap slope (bytes/min) — last 1h",
|
||||
"type": "query_value",
|
||||
"precision": 0,
|
||||
"requests": [
|
||||
{
|
||||
"queries": [
|
||||
{
|
||||
"name": "slope",
|
||||
"data_source": "metrics",
|
||||
"query": "derivative(avg:mesh.local.heap_free_bytes{service:meshtastic-firmware})",
|
||||
"aggregator": "avg"
|
||||
}
|
||||
],
|
||||
"response_format": "scalar"
|
||||
}
|
||||
],
|
||||
"conditional_formats": [
|
||||
{ "comparator": "<", "value": -100, "palette": "white_on_red" },
|
||||
{ "comparator": "<", "value": 0, "palette": "white_on_yellow" },
|
||||
{ "comparator": ">=", "value": 0, "palette": "white_on_green" }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"definition": {
|
||||
"title": "Total heap (bytes)",
|
||||
"type": "timeseries",
|
||||
"requests": [
|
||||
{
|
||||
"queries": [
|
||||
{
|
||||
"name": "total_heap",
|
||||
"data_source": "metrics",
|
||||
"query": "avg:mesh.local.heap_total_bytes{service:meshtastic-firmware} by {port}"
|
||||
}
|
||||
],
|
||||
"response_format": "timeseries",
|
||||
"display_type": "line"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"definition": {
|
||||
"title": "Battery level (%)",
|
||||
"type": "timeseries",
|
||||
"requests": [
|
||||
{
|
||||
"queries": [
|
||||
{
|
||||
"name": "battery",
|
||||
"data_source": "metrics",
|
||||
"query": "avg:mesh.device.battery_level{service:meshtastic-firmware} by {port}"
|
||||
}
|
||||
],
|
||||
"response_format": "timeseries",
|
||||
"display_type": "line"
|
||||
}
|
||||
],
|
||||
"yaxis": { "min": "0", "max": "105" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"definition": {
|
||||
"title": "Air utilization (TX %)",
|
||||
"type": "timeseries",
|
||||
"requests": [
|
||||
{
|
||||
"queries": [
|
||||
{
|
||||
"name": "airutil",
|
||||
"data_source": "metrics",
|
||||
"query": "avg:mesh.device.air_util_tx{service:meshtastic-firmware} by {port}"
|
||||
}
|
||||
],
|
||||
"response_format": "timeseries",
|
||||
"display_type": "line"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"definition": {
|
||||
"title": "Channel utilization (%)",
|
||||
"type": "timeseries",
|
||||
"requests": [
|
||||
{
|
||||
"queries": [
|
||||
{
|
||||
"name": "chutil",
|
||||
"data_source": "metrics",
|
||||
"query": "avg:mesh.device.channel_utilization{service:meshtastic-firmware} by {port}"
|
||||
}
|
||||
],
|
||||
"response_format": "timeseries",
|
||||
"display_type": "line"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"definition": {
|
||||
"title": "Log volume by level",
|
||||
"type": "timeseries",
|
||||
"show_legend": true,
|
||||
"requests": [
|
||||
{
|
||||
"response_format": "timeseries",
|
||||
"display_type": "bars",
|
||||
"queries": [
|
||||
{
|
||||
"name": "log_count",
|
||||
"data_source": "logs",
|
||||
"indexes": ["*"],
|
||||
"compute": { "aggregation": "count" },
|
||||
"search": { "query": "service:meshtastic-firmware" },
|
||||
"group_by": [
|
||||
{
|
||||
"facet": "@level",
|
||||
"limit": 10,
|
||||
"sort": { "order": "desc", "aggregation": "count" }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"definition": {
|
||||
"title": "Recent ERROR / CRIT firmware logs",
|
||||
"type": "list_stream",
|
||||
"requests": [
|
||||
{
|
||||
"response_format": "event_list",
|
||||
"query": {
|
||||
"data_source": "logs_stream",
|
||||
"query_string": "service:meshtastic-firmware (status:error OR @level:ERROR OR @level:CRIT)",
|
||||
"indexes": [],
|
||||
"sort": { "column": "timestamp", "order": "desc" }
|
||||
},
|
||||
"columns": [
|
||||
{ "field": "timestamp", "width": "auto" },
|
||||
{ "field": "host", "width": "auto" },
|
||||
{ "field": "@port", "width": "auto" },
|
||||
{ "field": "@level", "width": "auto" },
|
||||
{ "field": "@thread", "width": "auto" },
|
||||
{ "field": "message", "width": "stretch" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"definition": {
|
||||
"title": "Recorder marker events",
|
||||
"type": "list_stream",
|
||||
"requests": [
|
||||
{
|
||||
"response_format": "event_list",
|
||||
"query": {
|
||||
"data_source": "logs_stream",
|
||||
"query_string": "service:meshtastic-firmware @level:MARK",
|
||||
"indexes": [],
|
||||
"sort": { "column": "timestamp", "order": "desc" }
|
||||
},
|
||||
"columns": [
|
||||
{ "field": "timestamp", "width": "auto" },
|
||||
{ "field": "host", "width": "auto" },
|
||||
{ "field": "message", "width": "stretch" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"template_variables": [
|
||||
{
|
||||
"name": "port",
|
||||
"prefix": "port",
|
||||
"available_values": [],
|
||||
"default": "*"
|
||||
},
|
||||
{ "name": "host", "prefix": "host", "available_values": [], "default": "*" }
|
||||
],
|
||||
"layout_type": "ordered",
|
||||
"notify_list": [],
|
||||
"reflow_type": "auto"
|
||||
}
|
||||
Executable
+389
@@ -0,0 +1,389 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Forward selected recorder JSONL streams to Datadog.
|
||||
|
||||
Reads `.mtlog/logs.jsonl` and `.mtlog/telemetry.jsonl`, ships logs to the
|
||||
Logs Intake API and telemetry numerics to the Metrics v2 series API.
|
||||
Resumes from `.mtlog/.dd-cursor.json` so a daemon restart doesn't
|
||||
duplicate rows already shipped from the current live files.
|
||||
|
||||
This forwarder does not currently backfill rotated `.jsonl.gz` archives.
|
||||
If the recorder rotates before this process drains the live file, or the
|
||||
forwarder is down across a rotation, those older rows are skipped.
|
||||
|
||||
Usage:
|
||||
DD_API_KEY=... ./scripts/mtlog_to_datadog.py --tail
|
||||
./scripts/mtlog_to_datadog.py --once # catch up + exit
|
||||
./scripts/mtlog_to_datadog.py --since 3600 # backfill last hour from start
|
||||
|
||||
Default `DD_SITE` is `us5.datadoghq.com` — the team's Datadog instance.
|
||||
Override via `DD_SITE=...` env var or `--site` flag for one-offs.
|
||||
|
||||
The forwarder is a separate process by design — a Datadog outage or
|
||||
auth failure must not backpressure the recorder. We exit non-zero on
|
||||
fatal config errors (missing API key) and keep retrying on transient
|
||||
network/HTTP errors.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterator
|
||||
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
print(
|
||||
"requests is required. Install it in the mcp-server venv: "
|
||||
"uv pip install requests",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
_DEFAULT_LOG_DIR = Path(__file__).resolve().parents[1] / ".mtlog"
|
||||
_LOG_INTAKE_TPL = "https://http-intake.logs.{site}/api/v2/logs"
|
||||
_METRICS_TPL = "https://api.{site}/api/v2/series"
|
||||
_LOG_BATCH = 50
|
||||
_METRICS_BATCH = 100
|
||||
_MAX_RETRIES = 5
|
||||
_RETRY_BASE_S = 1.5
|
||||
|
||||
|
||||
# --- streaming JSONL with byte-position cursor -------------------------
|
||||
|
||||
|
||||
class _StreamReader:
|
||||
"""Reads a single rotating JSONL with cursor-based resume.
|
||||
|
||||
This tails only the live `.jsonl` file. The recorder rotates files
|
||||
(live `.jsonl` → `.YYYYMMDD-HHMMSS-uuuuuu-NNNNN.jsonl.gz`), which means
|
||||
the live file shrinks abruptly. We detect that via inode change OR live
|
||||
size < cursor position, and reset the live-file cursor to 0.
|
||||
"""
|
||||
|
||||
def __init__(self, path: Path, cursor: dict[str, Any]):
|
||||
self.path = path
|
||||
self.cursor = cursor
|
||||
|
||||
def _state(self) -> tuple[int, int]:
|
||||
"""Return (inode, size) for the live file. (0, 0) if missing."""
|
||||
try:
|
||||
st = self.path.stat()
|
||||
return (st.st_ino, st.st_size)
|
||||
except FileNotFoundError:
|
||||
return (0, 0)
|
||||
|
||||
def iter_new_records(self) -> Iterator[dict[str, Any]]:
|
||||
ino, size = self._state()
|
||||
last_ino = self.cursor.get("ino")
|
||||
last_pos = int(self.cursor.get("pos") or 0)
|
||||
if ino == 0:
|
||||
return
|
||||
if last_ino is not None and last_ino != ino:
|
||||
# Rotation happened. Start over.
|
||||
last_pos = 0
|
||||
if last_pos > size:
|
||||
# Live file truncated/shrunk under us — recorder rotated.
|
||||
last_pos = 0
|
||||
try:
|
||||
with self.path.open("r", encoding="utf-8") as fh:
|
||||
fh.seek(last_pos)
|
||||
for line in fh:
|
||||
line = line.rstrip("\n")
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
yield json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
last_pos = fh.tell()
|
||||
except FileNotFoundError:
|
||||
return
|
||||
self.cursor["ino"] = ino
|
||||
self.cursor["pos"] = last_pos
|
||||
|
||||
|
||||
def _load_cursor(path: Path) -> dict[str, Any]:
|
||||
if not path.exists():
|
||||
return {}
|
||||
try:
|
||||
return json.loads(path.read_text())
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return {}
|
||||
|
||||
|
||||
def _save_cursor(path: Path, data: dict[str, Any]) -> None:
|
||||
tmp = path.with_suffix(".json.tmp")
|
||||
tmp.write_text(json.dumps(data, separators=(",", ":")))
|
||||
tmp.replace(path)
|
||||
|
||||
|
||||
# --- Datadog clients ---------------------------------------------------
|
||||
|
||||
|
||||
class _DDSession:
|
||||
"""Pool one HTTPS session, share retry logic."""
|
||||
|
||||
def __init__(self, api_key: str, site: str, hostname: str) -> None:
|
||||
self.api_key = api_key
|
||||
self.site = site
|
||||
self.hostname = hostname
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update(
|
||||
{
|
||||
"DD-API-KEY": api_key,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
)
|
||||
|
||||
def _post(self, url: str, payload: Any) -> bool:
|
||||
for attempt in range(_MAX_RETRIES):
|
||||
try:
|
||||
resp = self.session.post(url, json=payload, timeout=30)
|
||||
except requests.RequestException as e:
|
||||
_wait_retry(attempt, f"network error: {e}")
|
||||
continue
|
||||
if 200 <= resp.status_code < 300:
|
||||
return True
|
||||
if resp.status_code in (408, 429, 500, 502, 503, 504):
|
||||
_wait_retry(
|
||||
attempt,
|
||||
f"HTTP {resp.status_code} retrying",
|
||||
)
|
||||
continue
|
||||
print(
|
||||
f"datadog refused: {resp.status_code} {resp.text[:200]}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return False
|
||||
return False
|
||||
|
||||
def send_logs(self, records: list[dict[str, Any]]) -> int:
|
||||
if not records:
|
||||
return 0
|
||||
url = _LOG_INTAKE_TPL.format(site=self.site)
|
||||
sent = 0
|
||||
for i in range(0, len(records), _LOG_BATCH):
|
||||
batch = records[i : i + _LOG_BATCH]
|
||||
if self._post(url, batch):
|
||||
sent += len(batch)
|
||||
return sent
|
||||
|
||||
def send_metrics(self, series: list[dict[str, Any]]) -> int:
|
||||
if not series:
|
||||
return 0
|
||||
url = _METRICS_TPL.format(site=self.site)
|
||||
sent = 0
|
||||
for i in range(0, len(series), _METRICS_BATCH):
|
||||
batch = series[i : i + _METRICS_BATCH]
|
||||
if self._post(url, {"series": batch}):
|
||||
sent += len(batch)
|
||||
return sent
|
||||
|
||||
|
||||
def _wait_retry(attempt: int, reason: str) -> None:
|
||||
wait = _RETRY_BASE_S * (2**attempt)
|
||||
print(
|
||||
f" retry {attempt + 1}/{_MAX_RETRIES} in {wait:.1f}s ({reason})",
|
||||
file=sys.stderr,
|
||||
)
|
||||
time.sleep(wait)
|
||||
|
||||
|
||||
# --- record → datadog payload ------------------------------------------
|
||||
|
||||
|
||||
def _log_record_to_dd(rec: dict[str, Any], host: str) -> dict[str, Any]:
|
||||
line = rec.get("line") or ""
|
||||
tags = [
|
||||
f"role:{rec.get('role')}",
|
||||
f"port:{rec.get('port')}",
|
||||
]
|
||||
level = rec.get("level")
|
||||
if level:
|
||||
tags.append(f"level:{level}")
|
||||
tag = rec.get("tag")
|
||||
if tag:
|
||||
tags.append(f"thread:{tag}")
|
||||
return {
|
||||
"ddsource": "meshtastic-firmware",
|
||||
"service": "meshtastic-firmware",
|
||||
"hostname": host,
|
||||
"message": line,
|
||||
"ddtags": ",".join(t for t in tags if t and "None" not in t),
|
||||
"timestamp": int((rec.get("ts") or time.time()) * 1000),
|
||||
"level": level,
|
||||
}
|
||||
|
||||
|
||||
def _telemetry_record_to_metrics(
|
||||
rec: dict[str, Any], host: str
|
||||
) -> list[dict[str, Any]]:
|
||||
fields = rec.get("fields") or {}
|
||||
if not isinstance(fields, dict):
|
||||
return []
|
||||
variant = rec.get("variant") or "unknown"
|
||||
ts = int(rec.get("ts") or time.time())
|
||||
out: list[dict[str, Any]] = []
|
||||
tags = []
|
||||
if rec.get("port"):
|
||||
tags.append(f"port:{rec['port']}")
|
||||
if rec.get("role"):
|
||||
tags.append(f"role:{rec['role']}")
|
||||
if rec.get("from_node"):
|
||||
tags.append(f"from_node:{rec['from_node']}")
|
||||
tags.append(f"variant:{variant}")
|
||||
for field, value in fields.items():
|
||||
if not isinstance(value, (int, float)) or isinstance(value, bool):
|
||||
continue
|
||||
metric = f"mesh.{variant}.{_metric_safe(field)}"
|
||||
out.append(
|
||||
{
|
||||
"metric": metric,
|
||||
"type": 3, # GAUGE
|
||||
"points": [{"timestamp": ts, "value": float(value)}],
|
||||
"tags": tags,
|
||||
"resources": [{"type": "host", "name": host}],
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _metric_safe(name: str) -> str:
|
||||
# Lowercase, replace non-alnum with underscore for safe metric names.
|
||||
return "".join(c.lower() if c.isalnum() else "_" for c in name)
|
||||
|
||||
|
||||
# --- main loop ---------------------------------------------------------
|
||||
|
||||
|
||||
def run(
|
||||
log_dir: Path,
|
||||
*,
|
||||
once: bool,
|
||||
since_seconds: float | None,
|
||||
poll_interval: float,
|
||||
dd: _DDSession,
|
||||
) -> int:
|
||||
cursor_path = log_dir / ".dd-cursor.json"
|
||||
cursors = _load_cursor(cursor_path)
|
||||
|
||||
# `--since` overrides cursor: rewind to (now-since) timestamp.
|
||||
# We can't seek by timestamp directly (cursor is byte position), so
|
||||
# we just reset cursors to 0 and let the time filter in iter_new
|
||||
# drop older records.
|
||||
cutoff_ts: float | None = None
|
||||
if since_seconds is not None:
|
||||
cursors = {}
|
||||
cutoff_ts = time.time() - since_seconds
|
||||
|
||||
sent_total = {"logs": 0, "telemetry": 0}
|
||||
|
||||
while True:
|
||||
# logs.jsonl → DD logs
|
||||
log_cursor = cursors.setdefault("logs", {})
|
||||
log_batch: list[dict[str, Any]] = []
|
||||
for rec in _StreamReader(log_dir / "logs.jsonl", log_cursor).iter_new_records():
|
||||
if cutoff_ts and (rec.get("ts") or 0) < cutoff_ts:
|
||||
continue
|
||||
log_batch.append(_log_record_to_dd(rec, dd.hostname))
|
||||
if log_batch:
|
||||
n = dd.send_logs(log_batch)
|
||||
sent_total["logs"] += n
|
||||
print(f"logs: sent {n}/{len(log_batch)}")
|
||||
|
||||
# telemetry.jsonl → DD metrics
|
||||
telem_cursor = cursors.setdefault("telemetry", {})
|
||||
metric_series: list[dict[str, Any]] = []
|
||||
for rec in _StreamReader(
|
||||
log_dir / "telemetry.jsonl", telem_cursor
|
||||
).iter_new_records():
|
||||
if cutoff_ts and (rec.get("ts") or 0) < cutoff_ts:
|
||||
continue
|
||||
metric_series.extend(_telemetry_record_to_metrics(rec, dd.hostname))
|
||||
if metric_series:
|
||||
n = dd.send_metrics(metric_series)
|
||||
sent_total["telemetry"] += n
|
||||
print(f"telemetry: sent {n}/{len(metric_series)} metric points")
|
||||
|
||||
_save_cursor(cursor_path, cursors)
|
||||
|
||||
if once:
|
||||
print(f"done. logs={sent_total['logs']} metrics={sent_total['telemetry']}")
|
||||
return 0
|
||||
time.sleep(poll_interval)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--log-dir",
|
||||
default=str(_DEFAULT_LOG_DIR),
|
||||
help="Path to .mtlog/ (default: mcp-server/.mtlog)",
|
||||
)
|
||||
mode = parser.add_mutually_exclusive_group()
|
||||
mode.add_argument("--once", action="store_true", help="Catch up then exit")
|
||||
mode.add_argument(
|
||||
"--tail",
|
||||
action="store_true",
|
||||
help="Daemon: poll forever (default)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--since",
|
||||
type=float,
|
||||
default=None,
|
||||
help="Backfill last N seconds. Resets cursor.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--poll-interval",
|
||||
type=float,
|
||||
default=5.0,
|
||||
help="Seconds between tail polls (default 5)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--site",
|
||||
default=os.environ.get("DD_SITE", "us5.datadoghq.com"),
|
||||
help=(
|
||||
"Datadog site. Default is the team's instance (us5.datadoghq.com). "
|
||||
"Override via DD_SITE env var or this flag."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--host",
|
||||
default=socket.gethostname(),
|
||||
help="Hostname tag (default: socket.gethostname())",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
api_key = os.environ.get("DD_API_KEY")
|
||||
if not api_key:
|
||||
print("DD_API_KEY env var required.", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
log_dir = Path(args.log_dir)
|
||||
if not log_dir.exists():
|
||||
print(
|
||||
f"log dir {log_dir} does not exist — start the mcp-server first.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
|
||||
dd = _DDSession(api_key=api_key, site=args.site, hostname=args.host)
|
||||
once = args.once and not args.tail
|
||||
return run(
|
||||
log_dir,
|
||||
once=once,
|
||||
since_seconds=args.since,
|
||||
poll_interval=args.poll_interval,
|
||||
dd=dd,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,3 @@
|
||||
"""Meshtastic MCP server — device discovery, PlatformIO tooling, and device admin."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
@@ -0,0 +1,11 @@
|
||||
"""Entry point for `python -m meshtastic_mcp`."""
|
||||
|
||||
from meshtastic_mcp.server import app
|
||||
|
||||
|
||||
def main() -> None:
|
||||
app.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,417 @@
|
||||
"""Device administration: owner, config, channels, messaging, admin actions.
|
||||
|
||||
All operations use the same `connect()` context manager so port selection,
|
||||
port-busy detection, and cleanup are handled uniformly.
|
||||
|
||||
Config writes use a dot-path: the first segment names a section (e.g.
|
||||
`"lora"` in LocalConfig or `"mqtt"` in LocalModuleConfig), remaining segments
|
||||
walk protobuf fields. Enum fields accept their string names (`"US"` for
|
||||
`lora.region`) so callers don't need to know the numeric values.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from google.protobuf import descriptor as pb_descriptor
|
||||
from google.protobuf import json_format
|
||||
from meshtastic.protobuf import localonly_pb2
|
||||
|
||||
from .connection import connect
|
||||
|
||||
|
||||
class AdminError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
LOCAL_CONFIG_SECTIONS = {f.name for f in localonly_pb2.LocalConfig.DESCRIPTOR.fields}
|
||||
MODULE_CONFIG_SECTIONS = {
|
||||
f.name for f in localonly_pb2.LocalModuleConfig.DESCRIPTOR.fields
|
||||
}
|
||||
|
||||
|
||||
def _require_confirm(confirm: bool, operation: str) -> None:
|
||||
if not confirm:
|
||||
raise AdminError(f"{operation} is destructive and requires confirm=True.")
|
||||
|
||||
|
||||
def _message_to_dict(msg: Any) -> dict[str, Any]:
|
||||
# `including_default_value_fields` was renamed to
|
||||
# `always_print_fields_with_no_presence` in protobuf 5.26+. Pick whichever
|
||||
# kwarg the installed version accepts so we work against both.
|
||||
kwargs: dict[str, Any] = {"preserving_proto_field_name": True}
|
||||
import inspect
|
||||
|
||||
sig = inspect.signature(json_format.MessageToDict)
|
||||
if "always_print_fields_with_no_presence" in sig.parameters:
|
||||
kwargs["always_print_fields_with_no_presence"] = False
|
||||
elif "including_default_value_fields" in sig.parameters:
|
||||
kwargs["including_default_value_fields"] = False
|
||||
return json_format.MessageToDict(msg, **kwargs)
|
||||
|
||||
|
||||
# ---------- owner ----------------------------------------------------------
|
||||
|
||||
|
||||
def set_owner(
|
||||
long_name: str,
|
||||
short_name: str | None = None,
|
||||
port: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
if short_name is not None and len(short_name) > 4:
|
||||
raise AdminError("short_name must be 4 characters or fewer")
|
||||
with connect(port=port) as iface:
|
||||
iface.localNode.setOwner(long_name=long_name, short_name=short_name)
|
||||
return {
|
||||
"ok": True,
|
||||
"long_name": long_name,
|
||||
"short_name": short_name,
|
||||
}
|
||||
|
||||
|
||||
# ---------- config reads ---------------------------------------------------
|
||||
|
||||
|
||||
def _section_container(node, section: str) -> tuple[Any, str]:
|
||||
"""Return (container_message, parent_name) for a section name.
|
||||
|
||||
Parent is 'localConfig' or 'moduleConfig' so callers know where to call
|
||||
writeConfig() after mutating.
|
||||
"""
|
||||
if section in LOCAL_CONFIG_SECTIONS:
|
||||
return getattr(node.localConfig, section), "localConfig"
|
||||
if section in MODULE_CONFIG_SECTIONS:
|
||||
return getattr(node.moduleConfig, section), "moduleConfig"
|
||||
raise AdminError(
|
||||
f"Unknown config section: {section!r}. "
|
||||
f"Valid sections: {sorted(LOCAL_CONFIG_SECTIONS | MODULE_CONFIG_SECTIONS)}"
|
||||
)
|
||||
|
||||
|
||||
def get_config(section: str | None = None, port: str | None = None) -> dict[str, Any]:
|
||||
"""Read one or all config sections.
|
||||
|
||||
`section` may be any name in LocalConfig (device, lora, position, power,
|
||||
network, display, bluetooth, security) or LocalModuleConfig (mqtt, serial,
|
||||
telemetry, ...). Omit `section` or pass `"all"` for everything.
|
||||
"""
|
||||
with connect(port=port) as iface:
|
||||
node = iface.localNode
|
||||
if section in (None, "all"):
|
||||
lc = _message_to_dict(node.localConfig)
|
||||
mc = _message_to_dict(node.moduleConfig)
|
||||
return {
|
||||
"config": {
|
||||
"localConfig": lc,
|
||||
"moduleConfig": mc,
|
||||
}
|
||||
}
|
||||
container, _parent = _section_container(node, section)
|
||||
return {"config": {section: _message_to_dict(container)}}
|
||||
|
||||
|
||||
# ---------- config writes --------------------------------------------------
|
||||
|
||||
|
||||
def _coerce_enum(field: pb_descriptor.FieldDescriptor, value: Any) -> int:
|
||||
"""Accept an enum value as either its int or its string name."""
|
||||
enum_type = field.enum_type
|
||||
if isinstance(value, bool):
|
||||
raise AdminError(f"{field.name}: expected enum {enum_type.name}, got bool")
|
||||
if isinstance(value, int):
|
||||
if enum_type.values_by_number.get(value) is None:
|
||||
raise AdminError(
|
||||
f"{field.name}: {value} is not a valid {enum_type.name} value"
|
||||
)
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
upper = value.upper()
|
||||
ev = enum_type.values_by_name.get(upper)
|
||||
if ev is None:
|
||||
valid = sorted(enum_type.values_by_name.keys())
|
||||
raise AdminError(
|
||||
f"{field.name}: {value!r} is not a valid {enum_type.name}. "
|
||||
f"Valid: {valid}"
|
||||
)
|
||||
return ev.number
|
||||
raise AdminError(
|
||||
f"{field.name}: expected enum {enum_type.name}, got {type(value).__name__}"
|
||||
)
|
||||
|
||||
|
||||
def _coerce_scalar(field: pb_descriptor.FieldDescriptor, value: Any) -> Any:
|
||||
t = field.type
|
||||
FT = pb_descriptor.FieldDescriptor
|
||||
if t == FT.TYPE_ENUM:
|
||||
return _coerce_enum(field, value)
|
||||
if t == FT.TYPE_BOOL:
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
return value.strip().lower() in ("true", "yes", "1", "on")
|
||||
if isinstance(value, int):
|
||||
return bool(value)
|
||||
if t in (
|
||||
FT.TYPE_INT32,
|
||||
FT.TYPE_INT64,
|
||||
FT.TYPE_UINT32,
|
||||
FT.TYPE_UINT64,
|
||||
FT.TYPE_SINT32,
|
||||
FT.TYPE_SINT64,
|
||||
FT.TYPE_FIXED32,
|
||||
FT.TYPE_FIXED64,
|
||||
):
|
||||
return int(value)
|
||||
if t in (FT.TYPE_FLOAT, FT.TYPE_DOUBLE):
|
||||
return float(value)
|
||||
if t == FT.TYPE_STRING:
|
||||
return str(value)
|
||||
if t == FT.TYPE_BYTES:
|
||||
if isinstance(value, (bytes, bytearray)):
|
||||
return bytes(value)
|
||||
return str(value).encode("utf-8")
|
||||
raise AdminError(
|
||||
f"{field.name}: unsupported field type {t}. Use raw protobuf for this field."
|
||||
)
|
||||
|
||||
|
||||
def _walk_to_field(
|
||||
root_msg: Any, path_segments: list[str]
|
||||
) -> tuple[Any, pb_descriptor.FieldDescriptor]:
|
||||
"""Walk `root_msg` by field names until the leaf; return (parent_msg, leaf_field_descriptor)."""
|
||||
msg = root_msg
|
||||
for i, name in enumerate(path_segments):
|
||||
desc = msg.DESCRIPTOR
|
||||
field = desc.fields_by_name.get(name)
|
||||
if field is None:
|
||||
trail = ".".join(path_segments[:i] or ["<root>"])
|
||||
valid = [f.name for f in desc.fields]
|
||||
raise AdminError(f"No field {name!r} in {trail}. Valid: {valid}")
|
||||
is_last = i == len(path_segments) - 1
|
||||
if is_last:
|
||||
return msg, field
|
||||
if field.type != pb_descriptor.FieldDescriptor.TYPE_MESSAGE:
|
||||
raise AdminError(
|
||||
f"{'.'.join(path_segments[:i+1])} is a scalar; cannot descend into it"
|
||||
)
|
||||
msg = getattr(msg, name)
|
||||
# path_segments was empty
|
||||
raise AdminError("Empty config path")
|
||||
|
||||
|
||||
def set_config(path: str, value: Any, port: str | None = None) -> dict[str, Any]:
|
||||
"""Set a single config field by dot-path and write it to the device.
|
||||
|
||||
Examples:
|
||||
set_config("lora.region", "US")
|
||||
set_config("lora.modem_preset", "LONG_FAST")
|
||||
set_config("device.role", "ROUTER")
|
||||
set_config("mqtt.enabled", True)
|
||||
set_config("mqtt.address", "mqtt.example.com")
|
||||
|
||||
"""
|
||||
segments = [s for s in path.split(".") if s]
|
||||
if not segments:
|
||||
raise AdminError("path cannot be empty")
|
||||
section = segments[0]
|
||||
|
||||
with connect(port=port) as iface:
|
||||
node = iface.localNode
|
||||
container, parent_name = _section_container(node, section)
|
||||
|
||||
# Treat the section as the root; the rest of the path walks into it.
|
||||
leaf_parent, field = _walk_to_field(container, segments[1:] or [])
|
||||
# Use `is_repeated` (modern upb protobuf API) rather than the
|
||||
# deprecated `label == LABEL_REPEATED` check — the C-extension
|
||||
# FieldDescriptor in protobuf >= 5.x doesn't expose `.label` at
|
||||
# all, and `is_repeated` is the supported replacement that works
|
||||
# across both the pure-python and upb backends.
|
||||
if field.is_repeated:
|
||||
raise AdminError(
|
||||
f"{path!r} is a repeated field; v1 only supports scalar sets. "
|
||||
"Use the raw meshtastic CLI for now."
|
||||
)
|
||||
old_raw = getattr(leaf_parent, field.name)
|
||||
coerced = _coerce_scalar(field, value)
|
||||
try:
|
||||
setattr(leaf_parent, field.name, coerced)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise AdminError(f"{path}: {exc}") from exc
|
||||
|
||||
node.writeConfig(section)
|
||||
|
||||
# Stringify enums for the response (so the caller can see the change in
|
||||
# the same vocabulary they used to set it).
|
||||
if field.type == pb_descriptor.FieldDescriptor.TYPE_ENUM:
|
||||
try:
|
||||
old_display = field.enum_type.values_by_number[old_raw].name
|
||||
new_display = field.enum_type.values_by_number[coerced].name
|
||||
except Exception:
|
||||
old_display, new_display = old_raw, coerced
|
||||
else:
|
||||
old_display, new_display = old_raw, coerced
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"path": path,
|
||||
"section": section,
|
||||
"parent": parent_name,
|
||||
"old_value": old_display,
|
||||
"new_value": new_display,
|
||||
}
|
||||
|
||||
|
||||
# ---------- channels -------------------------------------------------------
|
||||
|
||||
|
||||
def get_channel_url(
|
||||
include_all: bool = False, port: str | None = None
|
||||
) -> dict[str, Any]:
|
||||
with connect(port=port) as iface:
|
||||
url = iface.localNode.getURL(includeAll=include_all)
|
||||
return {"url": url}
|
||||
|
||||
|
||||
def set_channel_url(url: str, port: str | None = None) -> dict[str, Any]:
|
||||
with connect(port=port) as iface:
|
||||
# setURL replaces the channel set from the URL's contents. It does not
|
||||
# return a count; we infer by counting non-DISABLED channels after.
|
||||
iface.localNode.setURL(url)
|
||||
channels = iface.localNode.channels or []
|
||||
active = sum(1 for c in channels if getattr(c, "role", 0) != 0)
|
||||
return {"ok": True, "channels_imported": active}
|
||||
|
||||
|
||||
# ---------- messaging ------------------------------------------------------
|
||||
|
||||
|
||||
def send_text(
|
||||
text: str,
|
||||
to: str | int | None = None,
|
||||
channel_index: int = 0,
|
||||
want_ack: bool = False,
|
||||
port: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
destination = to if to is not None else "^all"
|
||||
with connect(port=port) as iface:
|
||||
packet = iface.sendText(
|
||||
text,
|
||||
destinationId=destination,
|
||||
wantAck=want_ack,
|
||||
channelIndex=channel_index,
|
||||
)
|
||||
packet_id = getattr(packet, "id", None)
|
||||
return {"ok": True, "packet_id": packet_id, "destination": destination}
|
||||
|
||||
|
||||
# ---------- diagnostics ----------------------------------------------------
|
||||
|
||||
|
||||
def set_debug_log_api(enabled: bool, port: str | None = None) -> dict[str, Any]:
|
||||
"""Toggle `config.security.debug_log_api_enabled` on the local node.
|
||||
|
||||
When enabled, firmware emits log lines as protobuf `LogRecord` messages
|
||||
over the StreamAPI instead of raw text. meshtastic-python surfaces them
|
||||
on pubsub topic `meshtastic.log.line`, which flows through the SAME
|
||||
SerialInterface our tests already hold open — no `pio device monitor`
|
||||
needed, no port-contention with admin/info calls.
|
||||
|
||||
Firmware gate: `src/SerialConsole.cpp` (`usingProtobufs &&
|
||||
config.security.debug_log_api_enabled`). Setting persists in NVS; it
|
||||
survives reboot. `factory_reset(full=False)` clears it unless it's
|
||||
re-applied after reset.
|
||||
|
||||
Previously-documented concurrency hazard (emitLogRecord sharing the
|
||||
main packet-emission buffers) has been fixed — see `StreamAPI.h`
|
||||
where the log path now owns dedicated `fromRadioScratchLog` /
|
||||
`txBufLog` buffers, and `StreamAPI::emitTxBuffer` +
|
||||
`StreamAPI::emitLogRecord` both serialize their `stream->write`
|
||||
calls via `streamLock`. Leaving the flag on under traffic is safe.
|
||||
"""
|
||||
with connect(port=port) as iface:
|
||||
sec = iface.localNode.localConfig.security
|
||||
sec.debug_log_api_enabled = bool(enabled)
|
||||
iface.localNode.writeConfig("security")
|
||||
return {"ok": True, "debug_log_api_enabled": bool(enabled)}
|
||||
|
||||
|
||||
# ---------- admin actions --------------------------------------------------
|
||||
|
||||
|
||||
def reboot(
|
||||
port: str | None = None, confirm: bool = False, seconds: int = 10
|
||||
) -> dict[str, Any]:
|
||||
_require_confirm(confirm, "reboot")
|
||||
with connect(port=port) as iface:
|
||||
iface.localNode.reboot(secs=seconds)
|
||||
return {"ok": True, "rebooting_in_s": seconds}
|
||||
|
||||
|
||||
def shutdown(
|
||||
port: str | None = None, confirm: bool = False, seconds: int = 10
|
||||
) -> dict[str, Any]:
|
||||
_require_confirm(confirm, "shutdown")
|
||||
with connect(port=port) as iface:
|
||||
iface.localNode.shutdown(secs=seconds)
|
||||
return {"ok": True, "shutting_down_in_s": seconds}
|
||||
|
||||
|
||||
def send_input_event(
|
||||
event_code: int | str,
|
||||
kb_char: int = 0,
|
||||
touch_x: int = 0,
|
||||
touch_y: int = 0,
|
||||
port: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Inject an InputBroker event (button press / key / gesture) into the UI.
|
||||
|
||||
Wraps `AdminMessage.send_input_event` (handled in firmware at
|
||||
src/modules/AdminModule.cpp::handleSendInputEvent). Local-only — no PKI
|
||||
warmup needed since the admin message is addressed to `my_node_num`.
|
||||
|
||||
`event_code` accepts an int, a case-insensitive name
|
||||
(`"RIGHT"` / `"input_broker_right"`), or an `InputEventCode`. The
|
||||
firmware-side enum lives in src/input/InputBroker.h and is mirrored in
|
||||
`meshtastic_mcp.input_events`.
|
||||
"""
|
||||
from meshtastic.protobuf import admin_pb2 # type: ignore[import-untyped]
|
||||
|
||||
from .input_events import coerce_event_code
|
||||
|
||||
code = coerce_event_code(event_code)
|
||||
if not 0 <= kb_char <= 255:
|
||||
raise ValueError(f"kb_char out of u8 range: {kb_char}")
|
||||
if not 0 <= touch_x <= 65535:
|
||||
raise ValueError(f"touch_x out of u16 range: {touch_x}")
|
||||
if not 0 <= touch_y <= 65535:
|
||||
raise ValueError(f"touch_y out of u16 range: {touch_y}")
|
||||
|
||||
with connect(port=port) as iface:
|
||||
msg = admin_pb2.AdminMessage()
|
||||
msg.send_input_event.event_code = code
|
||||
msg.send_input_event.kb_char = kb_char
|
||||
msg.send_input_event.touch_x = touch_x
|
||||
msg.send_input_event.touch_y = touch_y
|
||||
iface.localNode._sendAdmin(msg)
|
||||
return {"ok": True, "event_code": code, "kb_char": kb_char}
|
||||
|
||||
|
||||
def factory_reset(
|
||||
port: str | None = None, confirm: bool = False, full: bool = False
|
||||
) -> dict[str, Any]:
|
||||
"""Tell the node to factory-reset its config.
|
||||
|
||||
Works around a meshtastic-python 2.7.8 bug: `Node.factoryReset(full=True)`
|
||||
internally does `p.factory_reset_config = True` where the field is
|
||||
int32. protobuf 5.x rejects bool→int assignment as a TypeError. We build
|
||||
the AdminMessage directly with int values (1=non-full, 2=full) and call
|
||||
`_sendAdmin` to sidestep the SDK bug entirely.
|
||||
"""
|
||||
_require_confirm(confirm, "factory_reset")
|
||||
from meshtastic.protobuf import admin_pb2 # type: ignore[import-untyped]
|
||||
|
||||
with connect(port=port) as iface:
|
||||
msg = admin_pb2.AdminMessage()
|
||||
msg.factory_reset_config = 2 if full else 1
|
||||
iface.localNode._sendAdmin(msg)
|
||||
return {"ok": True, "full": full}
|
||||
@@ -0,0 +1,159 @@
|
||||
"""Board / PlatformIO env enumeration.
|
||||
|
||||
Parses `pio project config --json-output` — a nested list of
|
||||
`[section_name, [[key, value], ...]]` pairs — into a dict keyed by env name,
|
||||
extracting the `custom_meshtastic_*` metadata the firmware variants expose.
|
||||
|
||||
The parsed config is cached and invalidated when `platformio.ini`'s mtime
|
||||
changes, so subsequent calls don't pay the 1–2s pio startup cost.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from typing import Any
|
||||
|
||||
from . import config, pio
|
||||
|
||||
_CACHE_LOCK = threading.Lock()
|
||||
_CACHE: dict[str, Any] = {"mtime": None, "envs": None}
|
||||
|
||||
|
||||
def _parse_bool(value: Any) -> bool:
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
return value.strip().lower() in ("true", "yes", "1", "on")
|
||||
return bool(value)
|
||||
|
||||
|
||||
def _parse_int(value: Any) -> int | None:
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _parse_tags(value: Any) -> list[str]:
|
||||
if value is None:
|
||||
return []
|
||||
if isinstance(value, list):
|
||||
return [str(v).strip() for v in value if str(v).strip()]
|
||||
return [t.strip() for t in str(value).replace(",", " ").split() if t.strip()]
|
||||
|
||||
|
||||
def _env_record(env_name: str, items: list[list[Any]]) -> dict[str, Any]:
|
||||
"""Build a normalized dict for one env section."""
|
||||
d = dict(items)
|
||||
return {
|
||||
"env": env_name,
|
||||
"architecture": d.get("custom_meshtastic_architecture"),
|
||||
"hw_model": _parse_int(d.get("custom_meshtastic_hw_model")),
|
||||
"hw_model_slug": d.get("custom_meshtastic_hw_model_slug"),
|
||||
"display_name": d.get("custom_meshtastic_display_name"),
|
||||
"actively_supported": _parse_bool(
|
||||
d.get("custom_meshtastic_actively_supported")
|
||||
),
|
||||
"support_level": _parse_int(d.get("custom_meshtastic_support_level")),
|
||||
"board_level": d.get("board_level"), # "pr", "extra", or None
|
||||
"tags": _parse_tags(d.get("custom_meshtastic_tags")),
|
||||
"images": _parse_tags(d.get("custom_meshtastic_images")),
|
||||
"board": d.get("board"),
|
||||
"upload_speed": _parse_int(d.get("upload_speed")),
|
||||
"upload_protocol": d.get("upload_protocol"),
|
||||
"monitor_speed": _parse_int(d.get("monitor_speed")),
|
||||
"monitor_filters": d.get("monitor_filters") or [],
|
||||
"_raw": d, # Full dict for get_board
|
||||
}
|
||||
|
||||
|
||||
def _load_all() -> dict[str, dict[str, Any]]:
|
||||
"""Parse `pio project config` into `{env_name: record}`."""
|
||||
raw = pio.run_json(["project", "config"], timeout=pio.TIMEOUT_PROJECT_CONFIG)
|
||||
result: dict[str, dict[str, Any]] = {}
|
||||
for section_name, items in raw:
|
||||
if not isinstance(section_name, str) or not section_name.startswith("env:"):
|
||||
continue
|
||||
env_name = section_name.split(":", 1)[1]
|
||||
result[env_name] = _env_record(env_name, items)
|
||||
return result
|
||||
|
||||
|
||||
def _get_cached() -> dict[str, dict[str, Any]]:
|
||||
root = config.firmware_root()
|
||||
platformio_ini = root / "platformio.ini"
|
||||
try:
|
||||
mtime = platformio_ini.stat().st_mtime
|
||||
except FileNotFoundError:
|
||||
mtime = None
|
||||
|
||||
with _CACHE_LOCK:
|
||||
if _CACHE["envs"] is not None and _CACHE["mtime"] == mtime:
|
||||
return _CACHE["envs"]
|
||||
envs = _load_all()
|
||||
_CACHE["envs"] = envs
|
||||
_CACHE["mtime"] = mtime
|
||||
return envs
|
||||
|
||||
|
||||
def invalidate_cache() -> None:
|
||||
with _CACHE_LOCK:
|
||||
_CACHE["envs"] = None
|
||||
_CACHE["mtime"] = None
|
||||
|
||||
|
||||
def _public_record(rec: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Strip the `_raw` field for list outputs."""
|
||||
return {k: v for k, v in rec.items() if not k.startswith("_")}
|
||||
|
||||
|
||||
def list_boards(
|
||||
architecture: str | None = None,
|
||||
actively_supported_only: bool = False,
|
||||
query: str | None = None,
|
||||
board_level: str | None = None, # "release" | "pr" | "extra"
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Enumerate PlatformIO envs with Meshtastic metadata.
|
||||
|
||||
Filters are cumulative (AND). `board_level="release"` means envs with no
|
||||
explicit `board_level` set (the default release targets).
|
||||
"""
|
||||
envs = _get_cached()
|
||||
q = query.lower().strip() if query else None
|
||||
|
||||
out = []
|
||||
for rec in envs.values():
|
||||
if architecture and rec.get("architecture") != architecture:
|
||||
continue
|
||||
if actively_supported_only and not rec.get("actively_supported"):
|
||||
continue
|
||||
if board_level is not None:
|
||||
rec_level = rec.get("board_level")
|
||||
if board_level == "release":
|
||||
if rec_level not in (None, ""):
|
||||
continue
|
||||
elif rec_level != board_level:
|
||||
continue
|
||||
if q:
|
||||
display = (rec.get("display_name") or "").lower()
|
||||
env_name = rec.get("env", "").lower()
|
||||
slug = (rec.get("hw_model_slug") or "").lower()
|
||||
if q not in display and q not in env_name and q not in slug:
|
||||
continue
|
||||
out.append(_public_record(rec))
|
||||
|
||||
out.sort(key=lambda r: (r.get("architecture") or "", r.get("env")))
|
||||
return out
|
||||
|
||||
|
||||
def get_board(env: str) -> dict[str, Any]:
|
||||
"""Full metadata for one env, including the raw pio config dict."""
|
||||
envs = _get_cached()
|
||||
rec = envs.get(env)
|
||||
if rec is None:
|
||||
raise KeyError(
|
||||
f"Unknown env: {env!r}. Use list_boards() to see available envs."
|
||||
)
|
||||
public = _public_record(rec)
|
||||
public["raw_config"] = rec["_raw"]
|
||||
return public
|
||||
@@ -0,0 +1,286 @@
|
||||
"""Cross-platform USB-webcam capture for UI tests + the `capture_screen` tool.
|
||||
|
||||
Backends:
|
||||
- `opencv` — cv2.VideoCapture (AVFoundation on macOS, V4L2 on Linux).
|
||||
- `ffmpeg` — subprocess shelling out to the system `ffmpeg` binary. Slower
|
||||
per frame, but zero Python deps beyond stdlib.
|
||||
- `null` — no-op stub returning a 1×1 black PNG. Used when no camera is
|
||||
configured; keeps code paths alive without forcing every operator to
|
||||
hook up hardware.
|
||||
|
||||
Environment variables (read at `get_camera()` call time):
|
||||
- `MESHTASTIC_UI_CAMERA_BACKEND` — one of `opencv` / `ffmpeg` / `null` /
|
||||
`auto` (default). `auto` picks opencv if `cv2` imports, else ffmpeg if
|
||||
`ffmpeg --version` resolves, else null.
|
||||
- `MESHTASTIC_UI_CAMERA_DEVICE` — generic default (index or path).
|
||||
- `MESHTASTIC_UI_CAMERA_DEVICE_<ROLE>` — per-role override, e.g.
|
||||
`MESHTASTIC_UI_CAMERA_DEVICE_ESP32S3=0` for the OLED-bearing heltec-v3.
|
||||
Role suffix is uppercased before lookup.
|
||||
|
||||
Dependencies land in the optional `[ui]` extra; imports are lazy so clients
|
||||
without `opencv-python-headless` installed can still import this module.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
class CameraError(RuntimeError):
|
||||
"""Raised when a camera backend fails to initialize or capture."""
|
||||
|
||||
|
||||
class CameraBackend(Protocol):
|
||||
name: str
|
||||
|
||||
def capture(self) -> bytes:
|
||||
"""Return one PNG-encoded frame."""
|
||||
...
|
||||
|
||||
def close(self) -> None: ...
|
||||
|
||||
|
||||
# ---------- OpenCV backend -------------------------------------------------
|
||||
|
||||
|
||||
class OpenCVBackend:
|
||||
name = "opencv"
|
||||
|
||||
def __init__(self, device: int | str, warmup_frames: int = 5) -> None:
|
||||
try:
|
||||
import cv2 # type: ignore[import-untyped] # noqa: PLC0415
|
||||
except ImportError as exc:
|
||||
raise CameraError(
|
||||
"opencv backend requested but `cv2` is not installed. "
|
||||
"Install the mcp-server [ui] extra: pip install -e '.[ui]'"
|
||||
) from exc
|
||||
|
||||
self._cv2 = cv2
|
||||
device_arg: int | str
|
||||
if isinstance(device, str) and device.isdigit():
|
||||
device_arg = int(device)
|
||||
else:
|
||||
device_arg = device
|
||||
self._cap = cv2.VideoCapture(device_arg)
|
||||
if not self._cap.isOpened():
|
||||
raise CameraError(
|
||||
f"cv2.VideoCapture({device_arg!r}) failed to open. "
|
||||
"On macOS check TCC Camera permission; on Linux check /dev/video* and v4l2 access."
|
||||
)
|
||||
|
||||
# Drop the first few frames — auto-exposure + white-balance settle.
|
||||
for _ in range(warmup_frames):
|
||||
self._cap.read()
|
||||
# Detect a stuck black-frame camera early rather than silently
|
||||
# producing all-black captures.
|
||||
ok, frame = self._cap.read()
|
||||
if not ok or frame is None:
|
||||
self._cap.release()
|
||||
raise CameraError(f"camera {device_arg!r} opened but returned no frames")
|
||||
|
||||
def capture(self) -> bytes:
|
||||
cv2 = self._cv2
|
||||
ok, frame = self._cap.read()
|
||||
if not ok or frame is None:
|
||||
raise CameraError("cv2.VideoCapture.read() returned no frame")
|
||||
success, buf = cv2.imencode(".png", frame)
|
||||
if not success:
|
||||
raise CameraError("cv2.imencode('.png', ...) failed")
|
||||
return bytes(buf)
|
||||
|
||||
def close(self) -> None:
|
||||
try:
|
||||
self._cap.release()
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
|
||||
# ---------- ffmpeg subprocess backend --------------------------------------
|
||||
|
||||
|
||||
class FfmpegBackend:
|
||||
name = "ffmpeg"
|
||||
|
||||
def __init__(self, device: int | str) -> None:
|
||||
if shutil.which("ffmpeg") is None:
|
||||
raise CameraError("ffmpeg backend requested but `ffmpeg` is not on PATH")
|
||||
|
||||
self._device = str(device)
|
||||
# Platform-specific -f flag:
|
||||
# macOS → avfoundation (index like "0")
|
||||
# Linux → v4l2 (device like "/dev/video0" or "0")
|
||||
if sys.platform == "darwin":
|
||||
self._input_format = "avfoundation"
|
||||
self._input_spec = self._device # bare index for avfoundation
|
||||
else:
|
||||
self._input_format = "v4l2"
|
||||
self._input_spec = (
|
||||
self._device
|
||||
if self._device.startswith("/dev/")
|
||||
else f"/dev/video{self._device}"
|
||||
)
|
||||
|
||||
def capture(self) -> bytes:
|
||||
cmd = [
|
||||
"ffmpeg",
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-f",
|
||||
self._input_format,
|
||||
"-i",
|
||||
self._input_spec,
|
||||
"-frames:v",
|
||||
"1",
|
||||
"-f",
|
||||
"image2pipe",
|
||||
"-vcodec",
|
||||
"png",
|
||||
"-",
|
||||
]
|
||||
try:
|
||||
out = subprocess.run(
|
||||
cmd, capture_output=True, check=True, timeout=15 # noqa: S603
|
||||
)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
raise CameraError(
|
||||
f"ffmpeg capture failed (rc={exc.returncode}): {exc.stderr.decode(errors='replace')[:200]}"
|
||||
) from exc
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
raise CameraError("ffmpeg capture timed out after 15s") from exc
|
||||
return out.stdout
|
||||
|
||||
def close(self) -> None:
|
||||
pass # stateless — each capture spawns a new process
|
||||
|
||||
|
||||
# ---------- Null backend ---------------------------------------------------
|
||||
|
||||
|
||||
# A tiny valid 1×1 transparent PNG so callers always get a decodable image.
|
||||
_BLACK_1X1_PNG = bytes.fromhex(
|
||||
"89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489"
|
||||
"0000000d49444154789c6300010000000500010d0a2db40000000049454e44ae426082"
|
||||
)
|
||||
|
||||
|
||||
class NullBackend:
|
||||
name = "null"
|
||||
|
||||
def capture(self) -> bytes:
|
||||
return _BLACK_1X1_PNG
|
||||
|
||||
def close(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
# ---------- Factory --------------------------------------------------------
|
||||
|
||||
|
||||
def _resolve_device(role: str | None) -> str | None:
|
||||
if role:
|
||||
specific = os.environ.get(f"MESHTASTIC_UI_CAMERA_DEVICE_{role.upper()}")
|
||||
if specific:
|
||||
return specific
|
||||
return os.environ.get("MESHTASTIC_UI_CAMERA_DEVICE")
|
||||
|
||||
|
||||
def get_camera(role: str | None = None) -> CameraBackend:
|
||||
"""Return a CameraBackend for the given device role (e.g. `"esp32s3"`).
|
||||
|
||||
Falls back to `NullBackend` if no camera is configured or the selected
|
||||
backend fails to init — tests should treat captures as best-effort
|
||||
evidence, not a blocker.
|
||||
"""
|
||||
backend = os.environ.get("MESHTASTIC_UI_CAMERA_BACKEND", "auto").lower()
|
||||
device = _resolve_device(role)
|
||||
|
||||
if backend in ("null", "none") or device is None:
|
||||
return NullBackend()
|
||||
|
||||
if backend == "auto":
|
||||
# Prefer opencv if importable; fall back to ffmpeg; else null.
|
||||
try:
|
||||
import cv2 # type: ignore[import-untyped] # noqa: F401,PLC0415
|
||||
|
||||
backend = "opencv"
|
||||
except ImportError:
|
||||
backend = "ffmpeg" if shutil.which("ffmpeg") else "null"
|
||||
|
||||
if backend == "opencv":
|
||||
try:
|
||||
return OpenCVBackend(device)
|
||||
except CameraError as exc:
|
||||
warnings.warn(
|
||||
f"camera backend {backend!r} failed to initialize for device "
|
||||
f"{device!r}: {exc}; falling back to null backend",
|
||||
RuntimeWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return NullBackend()
|
||||
if backend == "ffmpeg":
|
||||
try:
|
||||
return FfmpegBackend(device)
|
||||
except CameraError as exc:
|
||||
warnings.warn(
|
||||
f"camera backend {backend!r} failed to initialize for device "
|
||||
f"{device!r}: {exc}; falling back to null backend",
|
||||
RuntimeWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return NullBackend()
|
||||
if backend == "null":
|
||||
return NullBackend()
|
||||
|
||||
raise CameraError(f"unknown MESHTASTIC_UI_CAMERA_BACKEND: {backend!r}")
|
||||
|
||||
|
||||
def save_capture(png_bytes: bytes, path: Path) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(png_bytes)
|
||||
|
||||
|
||||
def capture_to_file(role: str | None, path: Path) -> dict[str, object]:
|
||||
"""One-shot: open camera, capture, write PNG, close. Returns metadata."""
|
||||
started = time.monotonic()
|
||||
cam = get_camera(role)
|
||||
try:
|
||||
data = cam.capture()
|
||||
finally:
|
||||
cam.close()
|
||||
save_capture(data, path)
|
||||
return {
|
||||
"backend": cam.name,
|
||||
"path": str(path),
|
||||
"bytes": len(data),
|
||||
"elapsed_s": round(time.monotonic() - started, 3),
|
||||
}
|
||||
|
||||
|
||||
def _is_png(data: bytes) -> bool:
|
||||
return data.startswith(b"\x89PNG\r\n\x1a\n")
|
||||
|
||||
|
||||
# Exposed so callers can sanity-check a capture without a full PIL import.
|
||||
__all__ = [
|
||||
"CameraBackend",
|
||||
"CameraError",
|
||||
"FfmpegBackend",
|
||||
"NullBackend",
|
||||
"OpenCVBackend",
|
||||
"capture_to_file",
|
||||
"get_camera",
|
||||
"save_capture",
|
||||
]
|
||||
|
||||
# Keep `io` import used (pyflakes is picky) via a small guard used at import
|
||||
# time to normalize stdin/stdout if a subclass ever needs it.
|
||||
_ = io.BytesIO # noqa: SLF001
|
||||
@@ -0,0 +1,6 @@
|
||||
"""Command-line entry points that sit alongside the MCP server.
|
||||
|
||||
Modules here are loaded on-demand by `[project.scripts]` entries in
|
||||
`pyproject.toml`. They are NOT imported by `meshtastic_mcp.server` or the
|
||||
admin/info tool surface — the MCP server stays pure stdio JSON-RPC.
|
||||
"""
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Flash progress log tailer for ``meshtastic-mcp-test-tui``.
|
||||
|
||||
``pio.py`` / ``hw_tools.py`` tee subprocess output (``pio run -t upload``,
|
||||
``esptool erase_flash``, ``nrfutil dfu``, etc.) to ``tests/flash.log``
|
||||
line-by-line as it arrives — controlled by the ``MESHTASTIC_MCP_FLASH_LOG``
|
||||
env var that ``run-tests.sh`` sets. The TUI tails that file so the operator
|
||||
sees live flash progress in the pytest pane instead of 3 minutes of silence
|
||||
during ``test_00_bake``.
|
||||
|
||||
Separate from ``_fwlog.py`` because that one parses JSONL, this one
|
||||
streams plain text lines. Same daemon-thread + EOF-backoff structure.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
import threading
|
||||
import time
|
||||
from typing import Callable
|
||||
|
||||
|
||||
class FlashLogTailer(threading.Thread):
|
||||
"""Tail a plain-text log file, publish each stripped line via ``post``.
|
||||
|
||||
``post`` is invoked with a single ``str`` for every new line. Lines are
|
||||
stripped of trailing newlines; empty lines after stripping are dropped.
|
||||
|
||||
The file may not exist yet when this thread starts — it's truncated by
|
||||
``run-tests.sh`` at session start, but if the tailer races the shell,
|
||||
we tolerate FileNotFoundError for up to ``wait_s`` seconds.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
path: pathlib.Path,
|
||||
post: Callable[[str], None],
|
||||
stop: threading.Event,
|
||||
*,
|
||||
wait_s: float = 30.0,
|
||||
) -> None:
|
||||
super().__init__(daemon=True, name="flashlog-tail")
|
||||
self._path = path
|
||||
self._post = post
|
||||
self._stop = stop
|
||||
self._wait_s = wait_s
|
||||
|
||||
def run(self) -> None:
|
||||
deadline = time.monotonic() + self._wait_s
|
||||
while not self._path.is_file():
|
||||
if self._stop.is_set() or time.monotonic() > deadline:
|
||||
return
|
||||
time.sleep(0.1)
|
||||
try:
|
||||
fh = self._path.open("r", encoding="utf-8", errors="replace")
|
||||
except OSError:
|
||||
return
|
||||
try:
|
||||
while not self._stop.is_set():
|
||||
line = fh.readline()
|
||||
if not line:
|
||||
time.sleep(0.05)
|
||||
continue
|
||||
line = line.rstrip("\r\n")
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
self._post(line)
|
||||
except Exception:
|
||||
# A post failure (e.g. closed app) is terminal for this
|
||||
# thread but we still want to close the file handle.
|
||||
return
|
||||
finally:
|
||||
fh.close()
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Firmware log tail worker for ``meshtastic-mcp-test-tui``.
|
||||
|
||||
Complements v1's reportlog-tail worker. ``tests/conftest.py`` owns a
|
||||
session-scoped autouse fixture (``_firmware_log_stream``) that mirrors
|
||||
every ``meshtastic.log.line`` pubsub event to ``tests/fwlog.jsonl`` —
|
||||
one JSON object per line:
|
||||
|
||||
{"ts": 1729100000.123, "port": "/dev/cu.usbmodem1101", "line": "..."}
|
||||
|
||||
The TUI tails that file from a worker thread; each new line becomes a
|
||||
:class:`FirmwareLogLine` message posted to the App. Same pattern as the
|
||||
reportlog tail worker — truncate on launch, tolerate missing file for
|
||||
30 s, back off at EOF.
|
||||
|
||||
Kept in its own module so the (large) ``test_tui.py`` stays focused on
|
||||
the Textual App shell.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import pathlib
|
||||
import threading
|
||||
import time
|
||||
from typing import Any, Callable
|
||||
|
||||
|
||||
class FirmwareLogTailer(threading.Thread):
|
||||
"""Tail ``tests/fwlog.jsonl``, publish parsed records via ``post``.
|
||||
|
||||
``post`` is the App's ``post_message`` (or any callable that accepts a
|
||||
single payload arg). We pass parsed dicts rather than constructing
|
||||
Textual Message objects here — keeps this module free of the
|
||||
textual dependency so it's unit-testable in a bare venv.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path:
|
||||
Path to ``tests/fwlog.jsonl``. The file may not exist yet at
|
||||
startup — pytest only creates it once the session fixture runs.
|
||||
post:
|
||||
Callable invoked with a dict ``{"ts", "port", "line"}`` for every
|
||||
new line parsed from the file.
|
||||
stop:
|
||||
An event the App sets to signal shutdown.
|
||||
wait_s:
|
||||
How long to poll for the file's creation before giving up. Default
|
||||
30 s; pytest collection on a cold cache can be slow.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
path: pathlib.Path,
|
||||
post: Callable[[dict[str, Any]], None],
|
||||
stop: threading.Event,
|
||||
*,
|
||||
wait_s: float = 30.0,
|
||||
) -> None:
|
||||
super().__init__(daemon=True, name="fwlog-tail")
|
||||
self._path = path
|
||||
self._post = post
|
||||
self._stop = stop
|
||||
self._wait_s = wait_s
|
||||
|
||||
def run(self) -> None:
|
||||
deadline = time.monotonic() + self._wait_s
|
||||
while not self._path.is_file():
|
||||
if self._stop.is_set() or time.monotonic() > deadline:
|
||||
return
|
||||
time.sleep(0.1)
|
||||
try:
|
||||
fh = self._path.open("r", encoding="utf-8")
|
||||
except OSError:
|
||||
return
|
||||
try:
|
||||
while not self._stop.is_set():
|
||||
line = fh.readline()
|
||||
if not line:
|
||||
time.sleep(0.05)
|
||||
continue
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
record = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
# Defensive: require the three fields we rely on.
|
||||
if not isinstance(record, dict):
|
||||
continue
|
||||
if "line" not in record:
|
||||
continue
|
||||
self._post(record)
|
||||
finally:
|
||||
fh.close()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user