Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1acad6dc4 | ||
|
|
2a91d186eb | ||
|
|
4827498188 | ||
|
|
3a0c08b695 | ||
|
|
7bdff8ff70 | ||
|
|
35b0590408 | ||
|
|
cbddf07bc8 | ||
|
|
871194517d | ||
|
|
5a1d2b9ef4 | ||
|
|
9cd3a86938 | ||
|
|
748668b8e9 | ||
|
|
778d1ad90f | ||
|
|
75b7a7df4f | ||
|
|
8110887be2 | ||
|
|
5f734dabf9 | ||
|
|
039ad42758 | ||
|
|
593909c26b | ||
|
|
29a61dc75c | ||
|
|
eead467ce6 | ||
|
|
f3ae02c425 | ||
|
|
d9cb74e4dd | ||
|
|
7ff6641f97 | ||
|
|
f7548e7c25 | ||
|
|
b960121464 | ||
|
|
b074645586 | ||
|
|
811dd427dd | ||
|
|
1c06b702dc | ||
|
|
59b4993861 | ||
|
|
da61a0db7d | ||
|
|
8e99ffbe7e | ||
|
|
a23f923e64 | ||
|
|
0522039830 | ||
|
|
94bb21ecc7 | ||
|
|
1bcabb893b | ||
|
|
fefd424901 | ||
|
|
f63716c322 | ||
|
|
5e2ca8aed4 | ||
|
|
4553d1e0b1 | ||
|
|
784e3748e2 | ||
|
|
b11d29ff31 | ||
|
|
cdc47a2aea | ||
|
|
fcef46f4b0 | ||
|
|
bb86cf4e81 | ||
|
|
6ea0d5ebba | ||
|
|
b2bda3b07a | ||
|
|
b7a9555905 | ||
|
|
1eb860a3fc | ||
|
|
989b8620ba | ||
|
|
a0951f23c3 | ||
|
|
e19f531059 | ||
|
|
6c7ffa1054 | ||
|
|
f037ce2165 | ||
|
|
bc17d004ab | ||
|
|
06a6c3ee20 | ||
|
|
bfadf0c36a | ||
|
|
24c4162a75 | ||
|
|
b148fac340 | ||
|
|
8dde4eeee1 | ||
|
|
4d4e14600c | ||
|
|
0bd8dee346 | ||
|
|
2828dbe4ca | ||
|
|
a8c8fd7002 | ||
|
|
e7c02da24b | ||
|
|
7421953e8f | ||
|
|
9306e66067 | ||
|
|
7adfc3f992 | ||
|
|
ba9cadc14d | ||
|
|
924411de59 | ||
|
|
d9195944df | ||
|
|
83a98c81f6 | ||
|
|
837637b70c | ||
|
|
7b3f58875a | ||
|
|
b2d980fc25 | ||
|
|
2ed7bba5e7 | ||
|
|
22d50fe437 | ||
|
|
cde5a08bc5 | ||
|
|
55bf8c25fc | ||
|
|
399dde0f4b | ||
|
|
66971a0a26 | ||
|
|
4c24218afb | ||
|
|
4b4914736f | ||
|
|
a6b1a69630 | ||
|
|
28e705de5c | ||
|
|
fcb9ec0c2d | ||
|
|
a4b55bc6f2 | ||
|
|
b53fe7a1e7 | ||
|
|
db9fdd6794 | ||
|
|
3b4c66439d | ||
|
|
945f4780ea | ||
|
|
0e38a15d46 | ||
|
|
63bce1f01a | ||
|
|
4090d9f2b3 | ||
|
|
d50caf231b | ||
|
|
f396200d38 | ||
|
|
6c04c37294 | ||
|
|
34aa5e995b | ||
|
|
c8dac10348 | ||
|
|
aab4cd086f | ||
|
|
6208c243f9 | ||
|
|
3a87e746a8 | ||
|
|
cd04206334 | ||
|
|
2768080edf | ||
|
|
466cc4cecd | ||
|
|
fe90c49795 | ||
|
|
31418ca821 | ||
|
|
0ee5777c15 | ||
|
|
026213aab7 | ||
|
|
0cab43fb43 | ||
|
|
1341cd4078 | ||
|
|
4059202a5c | ||
|
|
d24d8806e1 | ||
|
|
a67eb15ad3 | ||
|
|
9e182a595c | ||
|
|
4587dc2d64 | ||
|
|
00fccd87f9 | ||
|
|
01bd4cfb73 | ||
|
|
125c1b7f13 | ||
|
|
d6cf67b6bc | ||
|
|
77f378dd53 | ||
|
|
16dcafa7fb | ||
|
|
4ac74edf38 | ||
|
|
69495dcd98 | ||
|
|
288d7a3e7b | ||
|
|
4990bf51e3 | ||
|
|
b2c8cbb78d | ||
|
|
8061f83704 | ||
|
|
a3b49b9028 | ||
|
|
17945884a5 | ||
|
|
1116f06139 | ||
|
|
c728cfdaf4 | ||
|
|
9322bcdb21 | ||
|
|
2f19a1d7a4 | ||
|
|
71c8143f72 | ||
|
|
222d3e6b62 | ||
|
|
2e6519bb98 | ||
|
|
726d539174 | ||
|
|
e7ee4bea18 | ||
|
|
efd2613bd7 | ||
|
|
f88bc732cc | ||
|
|
0746f8cfff | ||
|
|
0ac95e370b | ||
|
|
283ccb83d1 | ||
|
|
5319bc7c2c | ||
|
|
db694f2f24 | ||
|
|
814f1289f6 | ||
|
|
0b7556b590 | ||
|
|
f7e4ac3e43 | ||
|
|
c36ae159ed | ||
|
|
33e7f16c05 | ||
|
|
d3a86841b9 | ||
|
|
e14b8d385a | ||
|
|
8ce1a872eb | ||
|
|
abfa346630 | ||
|
|
2ffe2d829c | ||
|
|
bfaf6c6b20 | ||
|
|
d293d654a0 | ||
|
|
8384659608 | ||
|
|
a632d0133f | ||
|
|
f982b58d61 | ||
|
|
efb0b299b2 | ||
|
|
eb80d3141d | ||
|
|
3604c1255d | ||
|
|
22d63fa69c | ||
|
|
fc030d2d19 | ||
|
|
4a534f02a4 | ||
|
|
0ea408e12b | ||
|
|
644d0d4a15 | ||
|
|
f04746a928 | ||
|
|
1be2529fb9 | ||
|
|
959756abf1 | ||
|
|
c88b802e32 | ||
|
|
9f74fc11de | ||
|
|
2ef09d17b9 | ||
|
|
19d070c284 | ||
|
|
e24db2994b | ||
|
|
ac7a58cd45 | ||
|
|
53c21eb30d | ||
|
|
e51e6cad84 | ||
|
|
4890f7084f | ||
|
|
3fcbfe4370 | ||
|
|
da808cb43b | ||
|
|
82580c6798 | ||
|
|
016e68ec53 | ||
|
|
79f469ce71 | ||
|
|
0501e177e9 |
@@ -135,6 +135,99 @@ On top of authorization, any remote admin message that **mutates** state (not a
|
||||
- **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
|
||||
|
||||
```
|
||||
|
||||
@@ -4,9 +4,14 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
# trunk-ignore(checkov/CKV_GHA_7)
|
||||
target:
|
||||
type: string
|
||||
required: false
|
||||
description: Choose the target board, e.g. nrf52_promicro_diy_tcxo. If blank, will find available targets.
|
||||
arch:
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- esp32
|
||||
- esp32s3
|
||||
- esp32c3
|
||||
@@ -15,32 +20,18 @@ on:
|
||||
- rp2040
|
||||
- rp2350
|
||||
- stm32
|
||||
target:
|
||||
type: string
|
||||
required: false
|
||||
description: Choose the target board, e.g. nrf52_promicro_diy_tcxo. If blank, will find available targets.
|
||||
# find-target:
|
||||
# type: boolean
|
||||
# default: true
|
||||
# description: 'Find the available targets'
|
||||
description: Choose an arch to limit the search, or 'all' to search all architectures.
|
||||
default: all
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
find-targets:
|
||||
if: ${{ inputs.target == '' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch:
|
||||
- esp32
|
||||
- esp32s3
|
||||
- esp32c3
|
||||
- esp32c6
|
||||
- nrf52840
|
||||
- rp2040
|
||||
- rp2350
|
||||
- stm32
|
||||
- all
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -51,14 +42,37 @@ jobs:
|
||||
- run: pip install -U platformio
|
||||
- name: Generate matrix
|
||||
id: jsonStep
|
||||
env:
|
||||
BUILDTARGET: ${{ inputs.target }}
|
||||
MATRIXARCH: ${{ inputs.arch }}
|
||||
run: |
|
||||
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} --level extra)
|
||||
echo "Name: $GITHUB_REF_NAME" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Base: $GITHUB_BASE_REF" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Arch: ${{matrix.arch}}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Ref: $GITHUB_REF" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Targets:" >> $GITHUB_STEP_SUMMARY
|
||||
echo $TARGETS | jq -r 'sort_by(.board) |.[] | "- " + .board' >> $GITHUB_STEP_SUMMARY
|
||||
if [ "$BUILDTARGET" = "" ]; then
|
||||
echo "Name: $GITHUB_REF_NAME" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Base: $GITHUB_BASE_REF" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Arch: $MATRIXARCH" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Ref: $GITHUB_REF" >> $GITHUB_STEP_SUMMARY
|
||||
echo "## 🎯 The following target boards are available to build:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Platform | Board |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| -------- | ----- |" >> $GITHUB_STEP_SUMMARY
|
||||
echo $TARGETS | jq -r 'sort_by(.board) | sort_by(.platform) |.[] | "| " + .platform + " | " + .board + " |" ' >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "We build this one:" >> $GITHUB_STEP_SUMMARY
|
||||
ARCH=$(echo "$TARGETS" | jq --arg BUILDTARGET "$BUILDTARGET" -r '.[] | select(.board==$BUILDTARGET) | .platform')
|
||||
echo "| Platform | Board |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| -------- | ----- |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| $ARCH | "$BUILDTARGET" |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
if [[ "$ARCH" == "" ]]; then
|
||||
echo "## ❌ Error: Target "$BUILDTARGET" not found!" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "## ✅ Target "$BUILDTARGET" found, proceeding to build." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
echo "You may need to refresh this page to make the built firmware appear below." >> $GITHUB_STEP_SUMMARY
|
||||
echo "arch=$ARCH" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
outputs:
|
||||
arch: ${{ steps.jsonStep.outputs.arch }}
|
||||
|
||||
version:
|
||||
if: ${{ inputs.target != '' }}
|
||||
@@ -78,12 +92,12 @@ jobs:
|
||||
|
||||
build:
|
||||
if: ${{ inputs.target != '' && inputs.arch != 'native' }}
|
||||
needs: [version]
|
||||
needs: [version, find-targets]
|
||||
uses: ./.github/workflows/build_firmware.yml
|
||||
with:
|
||||
version: ${{ needs.version.outputs.long }}
|
||||
pio_env: ${{ inputs.target }}
|
||||
platform: ${{ inputs.arch }}
|
||||
platform: ${{ needs.find-targets.outputs.arch }}
|
||||
|
||||
gather-artifacts:
|
||||
permissions:
|
||||
|
||||
@@ -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
|
||||
|
||||
+13
@@ -56,3 +56,16 @@ CMakeLists.txt
|
||||
.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
|
||||
|
||||
@@ -34,6 +34,13 @@ 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.14.4
|
||||
|
||||
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:]))
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
Lora:
|
||||
## Ebyte E80-900M22S
|
||||
## This is a bit experimental
|
||||
##
|
||||
##
|
||||
Module: lr1121
|
||||
gpiochip: 1 # subtract 32 from the gpio numbers
|
||||
DIO3_TCXO_VOLTAGE: 1.8
|
||||
CS: 16 #pin6 / GPIO48 1C0
|
||||
IRQ: 23 #pin17 / GPIO55 1C7
|
||||
Busy: 22 #pin16 / GPIO54 1C6
|
||||
Reset: 25 #pin13 / GPIO57 1D1
|
||||
|
||||
|
||||
spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19)
|
||||
spiSpeed: 2000000
|
||||
|
||||
rfswitch_table:
|
||||
pins: [DIO5, DIO6, DIO7]
|
||||
MODE_STBY: [LOW, LOW, LOW]
|
||||
MODE_RX: [LOW, HIGH, LOW]
|
||||
MODE_TX: [HIGH, HIGH, LOW]
|
||||
MODE_TX_HP: [HIGH, LOW, LOW]
|
||||
MODE_TX_HF: [LOW, LOW, LOW]
|
||||
MODE_GNSS: [LOW, LOW, HIGH]
|
||||
MODE_WIFI: [LOW, LOW, LOW]
|
||||
|
||||
General:
|
||||
MACAddressSource: eth0
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
Lora:
|
||||
## Ebyte E80-900M22S
|
||||
## This is a bit experimental
|
||||
##
|
||||
##
|
||||
Module: lr1121
|
||||
gpiochip: 1 # subtract 32 from the gpio numbers
|
||||
DIO3_TCXO_VOLTAGE: 1.8
|
||||
CS: 16 #pin6 / GPIO48 1C0
|
||||
IRQ: 23 #pin17 / GPIO55 1C7
|
||||
Busy: 22 #pin16 / GPIO54 1C6
|
||||
Reset: 25 #pin13 / GPIO57 1D1
|
||||
|
||||
|
||||
spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19)
|
||||
spiSpeed: 2000000
|
||||
|
||||
rfswitch_table:
|
||||
pins:
|
||||
- DIO5
|
||||
- DIO6
|
||||
MODE_STBY:
|
||||
- LOW
|
||||
- LOW
|
||||
MODE_RX:
|
||||
- HIGH
|
||||
- LOW
|
||||
MODE_TX:
|
||||
- HIGH
|
||||
- HIGH
|
||||
MODE_TX_HP:
|
||||
- LOW
|
||||
- HIGH
|
||||
MODE_TX_HF:
|
||||
- LOW
|
||||
- LOW
|
||||
MODE_GNSS:
|
||||
- LOW
|
||||
- LOW
|
||||
MODE_WIFI:
|
||||
- LOW
|
||||
- LOW
|
||||
|
||||
General:
|
||||
MACAddressSource: eth0
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
Lora:
|
||||
## Ebyte E80-900M22S
|
||||
## This is a bit experimental
|
||||
##
|
||||
##
|
||||
Module: lr1121
|
||||
gpiochip: 1 # subtract 32 from the gpio numbers
|
||||
DIO3_TCXO_VOLTAGE: 1.8
|
||||
CS: 16 #pin6 / GPIO48 1C0
|
||||
IRQ: 23 #pin17 / GPIO55 1C7
|
||||
Busy: 22 #pin16 / GPIO54 1C6
|
||||
Reset: 25 #pin13 / GPIO57 1D1
|
||||
|
||||
|
||||
spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19)
|
||||
spiSpeed: 2000000
|
||||
|
||||
rfswitch_table:
|
||||
pins: [DIO5, DIO6, DIO7]
|
||||
MODE_STBY: [LOW, LOW, LOW]
|
||||
MODE_RX: [LOW, LOW, LOW]
|
||||
MODE_TX: [LOW, HIGH, LOW]
|
||||
MODE_TX_HP: [HIGH, LOW, LOW]
|
||||
# MODE_TX_HF: []
|
||||
# MODE_GNSS: []
|
||||
MODE_WIFI: [LOW, LOW, LOW]
|
||||
|
||||
General:
|
||||
MACAddressSource: eth0
|
||||
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:]))
|
||||
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:]))
|
||||
@@ -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,382 @@
|
||||
"""Fake NodeDB fixture push — Portduino file copy + hardware XModem upload.
|
||||
|
||||
The fixture pipeline is two-stage:
|
||||
1. `bin/gen-fake-nodedb-seed.py` produces a deterministic JSONL describing N
|
||||
fake-but-realistic peers. Committed under `test/fixtures/nodedb/`.
|
||||
2. `bin/seed-json-to-proto.py` compiles JSONL → binary v25 NodeDatabase
|
||||
protobuf with fresh wall-clock timestamps.
|
||||
|
||||
This module exposes `push_fake_nodedb(...)`, the MCP tool that:
|
||||
- target="portduino": compiles the JSONL into the device's prefs dir on
|
||||
the local filesystem (`~/.portduino/<config>/prefs/nodes.proto`).
|
||||
- target="hardware": compiles to a temp file, then streams it over the
|
||||
XModem protocol (via the meshtastic SerialInterface/BLEInterface +
|
||||
`meshtastic.xmodempacket` pubsub topic) to `/prefs/nodes.proto` on the
|
||||
device. Triggers a reboot so the firmware loads the new state on next
|
||||
boot.
|
||||
|
||||
XModem wire details (mirrors firmware impl at src/xmodem.cpp:115-260):
|
||||
* 128-byte chunks; final chunk padded to 128 B with 0x1A (SUB) bytes.
|
||||
* CRC16-CCITT (poly 0x1021, init 0x0000).
|
||||
* SOH/seq=0 carries the destination filename in `buffer.bytes`. ACK if
|
||||
`FSCom.open(filename, FILE_O_WRITE)` succeeds; NAK otherwise.
|
||||
* SOH/seq≥1 carries a 128-byte chunk. ACK = advance; NAK = retransmit.
|
||||
* EOT after the last chunk flushes + closes the file on-device.
|
||||
|
||||
Hardware push requires `confirm=True` (mirrors factory_reset / erase_and_flash
|
||||
in the .github/copilot-instructions.md "never do these without asking" list).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import hashlib
|
||||
import pathlib
|
||||
import queue
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from typing import Any, Literal
|
||||
|
||||
from .connection import connect, is_tcp_port
|
||||
|
||||
# Resolve repo root so the tool works regardless of mcp-server cwd.
|
||||
_REPO_ROOT = pathlib.Path(__file__).resolve().parents[3]
|
||||
_SEED_DIR = _REPO_ROOT / "test" / "fixtures" / "nodedb"
|
||||
_COMPILE_SCRIPT = _REPO_ROOT / "bin" / "seed-json-to-proto.py"
|
||||
|
||||
_DEFAULT_NODES_FILENAME = "/prefs/nodes.proto"
|
||||
_XMODEM_CHUNK = 128
|
||||
_XMODEM_SUB = 0x1A
|
||||
_ACK_TIMEOUT_INIT_S = 5.0
|
||||
_ACK_TIMEOUT_CHUNK_S = 2.0
|
||||
_MAX_CHUNK_RETRIES = 5
|
||||
|
||||
_VALID_SIZES = (250, 500, 1000, 2000)
|
||||
|
||||
|
||||
class FixtureError(RuntimeError):
|
||||
"""Raised for any fixture-push failure (compile, transport, ack timeout, …)."""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CRC16-CCITT (poly 0x1021, init 0x0000). Matches the firmware's `crc16_ccitt`.
|
||||
# Hand-rolled to avoid the optional `crcmod` dep.
|
||||
# ---------------------------------------------------------------------------
|
||||
def _crc16_ccitt(data: bytes, *, init: int = 0x0000) -> int:
|
||||
crc = init
|
||||
for b in data:
|
||||
crc ^= b << 8
|
||||
for _ in range(8):
|
||||
if crc & 0x8000:
|
||||
crc = ((crc << 1) ^ 0x1021) & 0xFFFF
|
||||
else:
|
||||
crc = (crc << 1) & 0xFFFF
|
||||
return crc
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Compile step — shells out to bin/seed-json-to-proto.py so the MCP module
|
||||
# doesn't have to duplicate the proto-encoding logic.
|
||||
# ---------------------------------------------------------------------------
|
||||
def _compile_proto(jsonl_path: pathlib.Path, out_path: pathlib.Path) -> None:
|
||||
if not _COMPILE_SCRIPT.is_file():
|
||||
raise FixtureError(f"compile script missing at {_COMPILE_SCRIPT}")
|
||||
cmd = [
|
||||
sys.executable,
|
||||
str(_COMPILE_SCRIPT),
|
||||
"--in",
|
||||
str(jsonl_path),
|
||||
"--out",
|
||||
str(out_path),
|
||||
]
|
||||
try:
|
||||
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
raise FixtureError(
|
||||
f"seed-json-to-proto.py failed (exit {exc.returncode}):\n"
|
||||
f" stdout: {exc.stdout}\n stderr: {exc.stderr}"
|
||||
) from exc
|
||||
|
||||
|
||||
def _resolve_seed_jsonl(size: int, custom: str | None) -> pathlib.Path:
|
||||
if custom is not None:
|
||||
p = pathlib.Path(custom).expanduser().resolve()
|
||||
if not p.is_file():
|
||||
raise FixtureError(f"custom_seed_jsonl not found: {p}")
|
||||
return p
|
||||
p = _SEED_DIR / f"seed_v25_{size:04d}.jsonl"
|
||||
if not p.is_file():
|
||||
raise FixtureError(
|
||||
f"missing committed seed at {p}. "
|
||||
f"Run `./bin/regen-fake-nodedbs.sh` to generate it."
|
||||
)
|
||||
return p
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Portduino push — file copy into ~/.portduino/<config>/prefs/
|
||||
# ---------------------------------------------------------------------------
|
||||
def _portduino_prefs_dir(config_name: str) -> pathlib.Path:
|
||||
home = pathlib.Path.home()
|
||||
return home / ".portduino" / config_name / "prefs"
|
||||
|
||||
|
||||
def _push_portduino(
|
||||
size: int,
|
||||
jsonl: pathlib.Path,
|
||||
portduino_config: str,
|
||||
backup_existing: bool,
|
||||
) -> dict[str, Any]:
|
||||
prefs = _portduino_prefs_dir(portduino_config)
|
||||
prefs.mkdir(parents=True, exist_ok=True)
|
||||
target = prefs / "nodes.proto"
|
||||
backed_up_to: str | None = None
|
||||
if backup_existing and target.is_file():
|
||||
ts = int(time.time())
|
||||
backup = prefs / f"nodes.proto.bak.{ts}"
|
||||
shutil.move(str(target), str(backup))
|
||||
backed_up_to = str(backup)
|
||||
_compile_proto(jsonl, target)
|
||||
raw = target.read_bytes()
|
||||
return {
|
||||
"transport": "portduino",
|
||||
"path": str(target),
|
||||
"bytes": len(raw),
|
||||
"sha256": hashlib.sha256(raw).hexdigest(),
|
||||
"jsonl_source": str(jsonl),
|
||||
"backed_up_to": backed_up_to,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hardware push — XModem over BLE/serial via the meshtastic Python interface.
|
||||
# ---------------------------------------------------------------------------
|
||||
@dataclasses.dataclass
|
||||
class _AckEvent:
|
||||
control: int
|
||||
seq: int
|
||||
|
||||
|
||||
def _wait_for_response(q: "queue.Queue[_AckEvent]", timeout_s: float) -> _AckEvent:
|
||||
try:
|
||||
return q.get(timeout=timeout_s)
|
||||
except queue.Empty as exc:
|
||||
raise FixtureError(
|
||||
f"XModem response timeout after {timeout_s:.1f}s — device not responding"
|
||||
) from exc
|
||||
|
||||
|
||||
def _push_hardware(
|
||||
size: int,
|
||||
jsonl: pathlib.Path,
|
||||
port: str | None,
|
||||
reboot_after: bool,
|
||||
) -> dict[str, Any]:
|
||||
# Lazy imports so the module loads even when the meshtastic deps aren't
|
||||
# available (e.g. CI in a Python env without the package installed).
|
||||
try:
|
||||
from meshtastic.protobuf import mesh_pb2, xmodem_pb2
|
||||
from pubsub import pub
|
||||
except ImportError as exc: # pragma: no cover — dep missing
|
||||
raise FixtureError(
|
||||
f"hardware push requires the meshtastic + pypubsub packages: {exc}"
|
||||
) from exc
|
||||
|
||||
if is_tcp_port(port):
|
||||
raise FixtureError(
|
||||
"hardware push over TCP/portduino is not supported — use "
|
||||
"target='portduino' to drop the fixture directly into the prefs dir."
|
||||
)
|
||||
|
||||
# Compile the fixture to a temp file with fresh timestamps.
|
||||
with tempfile.NamedTemporaryFile(suffix=".proto", delete=False) as tf:
|
||||
proto_path = pathlib.Path(tf.name)
|
||||
try:
|
||||
_compile_proto(jsonl, proto_path)
|
||||
payload = proto_path.read_bytes()
|
||||
finally:
|
||||
proto_path.unlink(missing_ok=True)
|
||||
|
||||
sha256 = hashlib.sha256(payload).hexdigest()
|
||||
total_bytes = len(payload)
|
||||
|
||||
# Subscribe to XModem responses BEFORE we open the interface, so we don't
|
||||
# race the first ACK that arrives during the SOH/seq=0 handshake.
|
||||
#
|
||||
# NB: the signature MUST declare every kwarg pypubsub will see for this
|
||||
# topic, or pubsub locks the topic spec to a smaller set (whichever
|
||||
# subscribe arrives first) and then *rejects* the meshtastic library's
|
||||
# publish call with `SenderUnknownMsgDataError: unknown ... interface`.
|
||||
# The meshtastic lib publishes both `packet=` and `interface=`
|
||||
# (mesh_interface.py:1389-1395), so both must appear here.
|
||||
response_q: "queue.Queue[_AckEvent]" = queue.Queue()
|
||||
|
||||
def _on_xmodem(packet: Any = None, interface: Any = None, **_kw: Any) -> None:
|
||||
if packet is None:
|
||||
return
|
||||
response_q.put(_AckEvent(control=int(packet.control), seq=int(packet.seq)))
|
||||
|
||||
pub.subscribe(_on_xmodem, "meshtastic.xmodempacket")
|
||||
|
||||
chunks_sent = 0
|
||||
retried = 0
|
||||
rebooted = False
|
||||
|
||||
XMC = xmodem_pb2.XModem.Control
|
||||
try:
|
||||
with connect(port=port) as iface:
|
||||
# 1) Send the filename (SOH, seq=0).
|
||||
init_pkt = xmodem_pb2.XModem(
|
||||
control=XMC.Value("SOH"),
|
||||
seq=0,
|
||||
buffer=_DEFAULT_NODES_FILENAME.encode("utf-8"),
|
||||
)
|
||||
iface._sendToRadio(mesh_pb2.ToRadio(xmodemPacket=init_pkt))
|
||||
ack = _wait_for_response(response_q, _ACK_TIMEOUT_INIT_S)
|
||||
if ack.control != XMC.Value("ACK"):
|
||||
raise FixtureError(
|
||||
f"device refused filename {_DEFAULT_NODES_FILENAME!r} "
|
||||
f"(got control={ack.control}, expected ACK). "
|
||||
f"Filesystem full or permissions issue?"
|
||||
)
|
||||
|
||||
# 2) Stream the payload in 128 B chunks.
|
||||
for offset in range(0, total_bytes, _XMODEM_CHUNK):
|
||||
chunk = payload[offset : offset + _XMODEM_CHUNK]
|
||||
if len(chunk) < _XMODEM_CHUNK:
|
||||
# Pad final chunk to 128 B with SUB. The trailing 0x1A bytes
|
||||
# become part of the file on-device, but nanopb ignores
|
||||
# bytes past the end of the top-level message.
|
||||
chunk = chunk + bytes([_XMODEM_SUB] * (_XMODEM_CHUNK - len(chunk)))
|
||||
seq = ((offset // _XMODEM_CHUNK) + 1) % 256
|
||||
# Retry loop on NAK / timeout.
|
||||
attempts = 0
|
||||
while True:
|
||||
pkt = xmodem_pb2.XModem(
|
||||
control=XMC.Value("SOH"),
|
||||
seq=seq,
|
||||
buffer=chunk,
|
||||
crc16=_crc16_ccitt(chunk),
|
||||
)
|
||||
iface._sendToRadio(mesh_pb2.ToRadio(xmodemPacket=pkt))
|
||||
ack = _wait_for_response(response_q, _ACK_TIMEOUT_CHUNK_S)
|
||||
if ack.control == XMC.Value("ACK"):
|
||||
chunks_sent += 1
|
||||
break
|
||||
if ack.control == XMC.Value("NAK"):
|
||||
attempts += 1
|
||||
retried += 1
|
||||
if attempts >= _MAX_CHUNK_RETRIES:
|
||||
# Abort: send CAN so the firmware removes the half-
|
||||
# written file via FSCom.remove(filename).
|
||||
iface._sendToRadio(
|
||||
mesh_pb2.ToRadio(
|
||||
xmodemPacket=xmodem_pb2.XModem(
|
||||
control=XMC.Value("CAN")
|
||||
)
|
||||
)
|
||||
)
|
||||
raise FixtureError(
|
||||
f"chunk seq={seq} NAK'd {attempts} times; "
|
||||
f"aborted transfer (file removed on-device)."
|
||||
)
|
||||
continue # retry the same chunk
|
||||
raise FixtureError(
|
||||
f"unexpected XModem control={ack.control} on seq={seq}"
|
||||
)
|
||||
|
||||
# 3) Tell the device we're done.
|
||||
iface._sendToRadio(
|
||||
mesh_pb2.ToRadio(
|
||||
xmodemPacket=xmodem_pb2.XModem(control=XMC.Value("EOT"))
|
||||
)
|
||||
)
|
||||
ack = _wait_for_response(response_q, _ACK_TIMEOUT_CHUNK_S)
|
||||
if ack.control != XMC.Value("ACK"):
|
||||
raise FixtureError(f"EOT not ACKed (got control={ack.control})")
|
||||
|
||||
# 4) Reboot so loadFromDisk picks up the new file.
|
||||
if reboot_after:
|
||||
iface.localNode.reboot(secs=1)
|
||||
rebooted = True
|
||||
finally:
|
||||
try:
|
||||
pub.unsubscribe(_on_xmodem, "meshtastic.xmodempacket")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
"transport": "hardware",
|
||||
"port": port,
|
||||
"filename_on_device": _DEFAULT_NODES_FILENAME,
|
||||
"bytes": total_bytes,
|
||||
"chunks_sent": chunks_sent,
|
||||
"retried": retried,
|
||||
"sha256": sha256,
|
||||
"jsonl_source": str(jsonl),
|
||||
"rebooted": rebooted,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public entry point — registered as an MCP tool in server.py.
|
||||
# ---------------------------------------------------------------------------
|
||||
def push_fake_nodedb(
|
||||
size: int,
|
||||
target: Literal["portduino", "hardware"] = "portduino",
|
||||
*,
|
||||
port: str | None = None,
|
||||
portduino_config: str = "default",
|
||||
backup_existing: bool = True,
|
||||
confirm: bool = False,
|
||||
reboot_after: bool = True,
|
||||
custom_seed_jsonl: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Compile a fresh-timestamp NodeDatabase fixture and push it to a device.
|
||||
|
||||
Args:
|
||||
size: 250, 500, 1000, or 2000 — selects which committed seed JSONL to use.
|
||||
target: "portduino" (file copy to ~/.portduino/<config>/prefs/) or
|
||||
"hardware" (XModem upload to /prefs/nodes.proto + reboot).
|
||||
port: required for target="hardware". Serial path (e.g. /dev/cu.usbmodemXXXX)
|
||||
or BLE identifier. TCP endpoints are rejected — use target="portduino"
|
||||
instead.
|
||||
portduino_config: which Portduino instance dir under ~/.portduino/. Default "default".
|
||||
backup_existing: portduino only. Move nodes.proto -> nodes.proto.bak.<ts>
|
||||
if present, so you can roll back.
|
||||
confirm: required True for target="hardware" (writes flash + reboots).
|
||||
reboot_after: hardware only. If True, send a 1-second reboot after the
|
||||
final ACK so loadFromDisk picks up the new file at next boot.
|
||||
custom_seed_jsonl: override the committed JSONL. Use to push a hand-edited
|
||||
test scenario.
|
||||
|
||||
Returns:
|
||||
dict with transport, bytes, sha256, etc. — depends on target.
|
||||
|
||||
"""
|
||||
if size not in _VALID_SIZES:
|
||||
raise FixtureError(
|
||||
f"size must be one of {_VALID_SIZES}; got {size!r}. "
|
||||
f"Add a new committed seed if you need a different cardinality."
|
||||
)
|
||||
|
||||
jsonl = _resolve_seed_jsonl(size, custom_seed_jsonl)
|
||||
|
||||
if target == "portduino":
|
||||
return _push_portduino(size, jsonl, portduino_config, backup_existing)
|
||||
|
||||
if target == "hardware":
|
||||
if not confirm:
|
||||
raise FixtureError(
|
||||
"hardware push writes flash and triggers a reboot — pass confirm=True."
|
||||
)
|
||||
if not port:
|
||||
raise FixtureError(
|
||||
"target='hardware' requires a port (e.g. /dev/cu.usbmodemXXXX)."
|
||||
)
|
||||
return _push_hardware(size, jsonl, port, reboot_after)
|
||||
|
||||
raise FixtureError(f"unknown target {target!r}; expected 'portduino' or 'hardware'")
|
||||
@@ -15,6 +15,7 @@ from . import (
|
||||
admin,
|
||||
boards,
|
||||
devices,
|
||||
fixtures,
|
||||
flash,
|
||||
hw_tools,
|
||||
info,
|
||||
@@ -963,3 +964,45 @@ def recorder_export(
|
||||
dest_dir=dest_dir,
|
||||
streams=streams,
|
||||
)
|
||||
|
||||
|
||||
# ---------- Fixture / test-data push --------------------------------------
|
||||
|
||||
|
||||
@app.tool()
|
||||
def push_fake_nodedb(
|
||||
size: int,
|
||||
target: str = "portduino",
|
||||
port: str | None = None,
|
||||
portduino_config: str = "default",
|
||||
backup_existing: bool = True,
|
||||
confirm: bool = False,
|
||||
reboot_after: bool = True,
|
||||
custom_seed_jsonl: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Push a fake-NodeDB v25 fixture (250/500/1000/2000 nodes) onto a device.
|
||||
|
||||
Two transports:
|
||||
target="portduino" — file copy to ~/.portduino/<portduino_config>/prefs/nodes.proto.
|
||||
Fast, no device connection needed.
|
||||
target="hardware" — XModem upload over serial/BLE to /prefs/nodes.proto.
|
||||
Requires `port` + `confirm=True`. Triggers a reboot
|
||||
so loadFromDisk picks up the new file at next boot.
|
||||
|
||||
Compiles a fresh-timestamp proto from the committed JSONL seed under
|
||||
test/fixtures/nodedb/seed_v25_<N>.jsonl each invocation, so the loaded
|
||||
NodeDB always looks "recent" to the connecting phone. Structural data
|
||||
(names, IDs, positions, telemetries) is deterministic per the seed.
|
||||
|
||||
Override the JSONL via `custom_seed_jsonl` to push a hand-edited scenario.
|
||||
"""
|
||||
return fixtures.push_fake_nodedb(
|
||||
size=size,
|
||||
target=target, # type: ignore[arg-type]
|
||||
port=port,
|
||||
portduino_config=portduino_config,
|
||||
backup_existing=backup_existing,
|
||||
confirm=confirm,
|
||||
reboot_after=reboot_after,
|
||||
custom_seed_jsonl=custom_seed_jsonl,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
"""Tests for the fake-NodeDB fixture pipeline (bin/gen-fake-nodedb-seed.py
|
||||
+ bin/seed-json-to-proto.py + mcp-server fixtures.push_fake_nodedb).
|
||||
|
||||
Lives under tests/unit/ because none of these touch real hardware — they
|
||||
shell out to the bin/ scripts and decode the resulting protobufs in-process.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = pathlib.Path(__file__).resolve().parents[3]
|
||||
SEED_GEN = REPO_ROOT / "bin" / "gen-fake-nodedb-seed.py"
|
||||
COMPILE = REPO_ROOT / "bin" / "seed-json-to-proto.py"
|
||||
FIXTURES_DIR = REPO_ROOT / "test" / "fixtures" / "nodedb"
|
||||
|
||||
# Ensure the locally-generated Python protobuf bindings are importable.
|
||||
# These live under `meshtastic_v25` (not `meshtastic`) so they don't shadow
|
||||
# the PyPI `meshtastic` package that the rest of the mcp-server depends on.
|
||||
_BINDINGS_DIR = REPO_ROOT / "bin" / "_generated"
|
||||
if _BINDINGS_DIR.is_dir() and str(_BINDINGS_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(_BINDINGS_DIR))
|
||||
|
||||
try:
|
||||
from meshtastic_v25.deviceonly_pb2 import (
|
||||
NodeDatabase, # type: ignore[import-not-found]
|
||||
)
|
||||
except ImportError:
|
||||
NodeDatabase = None # type: ignore[assignment]
|
||||
|
||||
|
||||
def _require_v25_bindings() -> None:
|
||||
if NodeDatabase is None:
|
||||
pytest.skip(
|
||||
"v25 Python protobuf bindings missing; run `./bin/regen-py-protos.sh`."
|
||||
)
|
||||
if "positions" not in NodeDatabase.DESCRIPTOR.fields_by_name:
|
||||
pytest.skip(
|
||||
"Loaded NodeDatabase predates v25 — run `./bin/regen-py-protos.sh`."
|
||||
)
|
||||
|
||||
|
||||
def _run(cmd: list[str]) -> None:
|
||||
subprocess.check_call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Seed generator: deterministic for given --seed (no wall-clock dependence).
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_seed_generator_is_deterministic(tmp_path: pathlib.Path) -> None:
|
||||
a = tmp_path / "a.jsonl"
|
||||
b = tmp_path / "b.jsonl"
|
||||
_run(
|
||||
[
|
||||
sys.executable,
|
||||
str(SEED_GEN),
|
||||
"--count",
|
||||
"100",
|
||||
"--seed",
|
||||
"42",
|
||||
"--out",
|
||||
str(a),
|
||||
]
|
||||
)
|
||||
# Sleep so any sneaky wall-clock leak in the generator would surface as
|
||||
# a byte diff between the two runs.
|
||||
time.sleep(0.8)
|
||||
_run(
|
||||
[
|
||||
sys.executable,
|
||||
str(SEED_GEN),
|
||||
"--count",
|
||||
"100",
|
||||
"--seed",
|
||||
"42",
|
||||
"--out",
|
||||
str(b),
|
||||
]
|
||||
)
|
||||
assert a.read_bytes() == b.read_bytes()
|
||||
|
||||
|
||||
def test_seed_generator_meta_line(tmp_path: pathlib.Path) -> None:
|
||||
out = tmp_path / "seed.jsonl"
|
||||
_run(
|
||||
[
|
||||
sys.executable,
|
||||
str(SEED_GEN),
|
||||
"--count",
|
||||
"50",
|
||||
"--seed",
|
||||
"1",
|
||||
"--out",
|
||||
str(out),
|
||||
]
|
||||
)
|
||||
lines = out.read_text(encoding="utf-8").splitlines()
|
||||
assert len(lines) == 51 # 1 meta + 50 nodes
|
||||
meta = json.loads(lines[0])
|
||||
assert "_meta" in meta
|
||||
assert meta["_meta"]["version"] == 25
|
||||
assert meta["_meta"]["count"] == 50
|
||||
assert meta["_meta"]["seed"] == 1
|
||||
|
||||
|
||||
def test_seed_only_uses_active_hardware_and_roles(tmp_path: pathlib.Path) -> None:
|
||||
"""Confirm no deprecated roles + no off-list HW models leak through."""
|
||||
out = tmp_path / "seed.jsonl"
|
||||
_run(
|
||||
[
|
||||
sys.executable,
|
||||
str(SEED_GEN),
|
||||
"--count",
|
||||
"500",
|
||||
"--seed",
|
||||
"7",
|
||||
"--out",
|
||||
str(out),
|
||||
]
|
||||
)
|
||||
forbidden_roles = {"ROUTER_CLIENT", "REPEATER"}
|
||||
forbidden_hw = {
|
||||
"TLORA_V1",
|
||||
"TLORA_V2",
|
||||
"TLORA_V1_1P3",
|
||||
"TLORA_V2_1_1P6",
|
||||
"TLORA_V2_1_1P8",
|
||||
"HELTEC_V1",
|
||||
"HELTEC_V2_0",
|
||||
"HELTEC_V2_1",
|
||||
"TBEAM",
|
||||
"TBEAM_V0P7",
|
||||
"NANO_G1",
|
||||
"NANO_G1_EXPLORER",
|
||||
"NANO_G2_ULTRA",
|
||||
"STATION_G1",
|
||||
"STATION_G2",
|
||||
"PORTDUINO",
|
||||
"ANDROID_SIM",
|
||||
"DIY_V1",
|
||||
"LORA_RELAY_V1",
|
||||
"NRF52840_PCA10059",
|
||||
"NRF52_UNKNOWN",
|
||||
"DR_DEV",
|
||||
"GENIEBLOCKS",
|
||||
"M5STACK",
|
||||
"RP2040_LORA",
|
||||
"PPR",
|
||||
}
|
||||
for raw in out.read_text(encoding="utf-8").splitlines()[1:]:
|
||||
node = json.loads(raw)
|
||||
assert node["role"] not in forbidden_roles, f"deprecated role: {node['role']}"
|
||||
assert (
|
||||
node["hw_model"] not in forbidden_hw
|
||||
), f"non-tier-1 HW: {node['hw_model']}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Compile step + committed seeds.
|
||||
# ---------------------------------------------------------------------------
|
||||
@pytest.mark.parametrize("size", [250, 500, 1000, 2000])
|
||||
def test_committed_seed_compiles_and_decodes(size: int, tmp_path: pathlib.Path) -> None:
|
||||
_require_v25_bindings()
|
||||
proto = tmp_path / "out.proto"
|
||||
jsonl = FIXTURES_DIR / f"seed_v25_{size:04d}.jsonl"
|
||||
if not jsonl.is_file():
|
||||
pytest.skip(f"{jsonl} not present — run ./bin/regen-fake-nodedbs.sh")
|
||||
_run([sys.executable, str(COMPILE), "--in", str(jsonl), "--out", str(proto)])
|
||||
|
||||
db = NodeDatabase()
|
||||
db.ParseFromString(proto.read_bytes())
|
||||
assert db.version == 25
|
||||
assert len(db.nodes) == size
|
||||
nums = {n.num for n in db.nodes}
|
||||
assert len(nums) == size, "node numbers must be unique"
|
||||
assert all(n.long_name and n.short_name for n in db.nodes)
|
||||
assert all(len(n.long_name) <= 24 for n in db.nodes) # max_size:25 - NUL
|
||||
|
||||
# Coverage sanity (±10pp tolerance for binomial fluctuation).
|
||||
def in_range(actual: int, expected_ratio: float, tol_pp: float = 0.10) -> bool:
|
||||
lo = max(0, int((expected_ratio - tol_pp) * size))
|
||||
hi = min(size, int((expected_ratio + tol_pp) * size))
|
||||
return lo <= actual <= hi
|
||||
|
||||
assert in_range(len(db.positions), 0.85)
|
||||
assert in_range(len(db.telemetry), 0.70)
|
||||
assert in_range(len(db.environment), 0.25)
|
||||
assert in_range(len(db.status), 0.40)
|
||||
|
||||
|
||||
def test_compile_freshens_timestamps(tmp_path: pathlib.Path) -> None:
|
||||
"""Same JSONL compiled twice → identical structure, different timestamps."""
|
||||
_require_v25_bindings()
|
||||
jsonl = FIXTURES_DIR / "seed_v25_0250.jsonl"
|
||||
if not jsonl.is_file():
|
||||
pytest.skip("250-node seed not present — run ./bin/regen-fake-nodedbs.sh")
|
||||
a = tmp_path / "a.proto"
|
||||
b = tmp_path / "b.proto"
|
||||
_run([sys.executable, str(COMPILE), "--in", str(jsonl), "--out", str(a)])
|
||||
time.sleep(1.2)
|
||||
_run([sys.executable, str(COMPILE), "--in", str(jsonl), "--out", str(b)])
|
||||
|
||||
da = NodeDatabase()
|
||||
db_ = NodeDatabase()
|
||||
da.ParseFromString(a.read_bytes())
|
||||
db_.ParseFromString(b.read_bytes())
|
||||
|
||||
# Zero out timestamp fields and confirm everything else is byte-identical.
|
||||
for d in (da, db_):
|
||||
for n in d.nodes:
|
||||
n.last_heard = 0
|
||||
for p in d.positions:
|
||||
p.position.time = 0
|
||||
assert da.SerializeToString() == db_.SerializeToString()
|
||||
|
||||
# Re-load fresh copies to confirm timestamps actually moved.
|
||||
aa = NodeDatabase()
|
||||
bb = NodeDatabase()
|
||||
aa.ParseFromString(a.read_bytes())
|
||||
bb.ParseFromString(b.read_bytes())
|
||||
aa_max = max(n.last_heard for n in aa.nodes if n.last_heard)
|
||||
bb_max = max(n.last_heard for n in bb.nodes if n.last_heard)
|
||||
assert bb_max >= aa_max
|
||||
assert bb_max - aa_max < 5 # within a few seconds
|
||||
|
||||
|
||||
def test_compile_pinned_now_epoch_is_byte_identical(tmp_path: pathlib.Path) -> None:
|
||||
"""With --now-epoch pinned, two compiles produce identical bytes."""
|
||||
_require_v25_bindings()
|
||||
jsonl = FIXTURES_DIR / "seed_v25_0250.jsonl"
|
||||
if not jsonl.is_file():
|
||||
pytest.skip("250-node seed not present")
|
||||
a = tmp_path / "a.proto"
|
||||
b = tmp_path / "b.proto"
|
||||
for o in (a, b):
|
||||
_run(
|
||||
[
|
||||
sys.executable,
|
||||
str(COMPILE),
|
||||
"--in",
|
||||
str(jsonl),
|
||||
"--now-epoch",
|
||||
"1700000000",
|
||||
"--out",
|
||||
str(o),
|
||||
]
|
||||
)
|
||||
assert a.read_bytes() == b.read_bytes()
|
||||
|
||||
|
||||
def test_compile_timestamps_are_recent(tmp_path: pathlib.Path) -> None:
|
||||
_require_v25_bindings()
|
||||
jsonl = FIXTURES_DIR / "seed_v25_0250.jsonl"
|
||||
if not jsonl.is_file():
|
||||
pytest.skip("250-node seed not present")
|
||||
out = tmp_path / "out.proto"
|
||||
_run([sys.executable, str(COMPILE), "--in", str(jsonl), "--out", str(out)])
|
||||
db = NodeDatabase()
|
||||
db.ParseFromString(out.read_bytes())
|
||||
now = int(time.time())
|
||||
# No timestamp older than 7 days, none in the future.
|
||||
for n in db.nodes:
|
||||
if n.last_heard:
|
||||
assert now - 7 * 86400 <= n.last_heard <= now
|
||||
# At least half should be within the last hour
|
||||
# (matches expovariate(mean=3600s)).
|
||||
recent = sum(1 for n in db.nodes if n.last_heard and n.last_heard >= now - 3600)
|
||||
assert recent >= 0.4 * len(db.nodes)
|
||||
|
||||
|
||||
def test_compile_hand_edit_round_trip(tmp_path: pathlib.Path) -> None:
|
||||
"""Edit one JSONL line, recompile, confirm edit appears in the proto."""
|
||||
_require_v25_bindings()
|
||||
src = FIXTURES_DIR / "seed_v25_0250.jsonl"
|
||||
if not src.is_file():
|
||||
pytest.skip("250-node seed not present")
|
||||
dst = tmp_path / "edited.jsonl"
|
||||
lines = src.read_text(encoding="utf-8").splitlines()
|
||||
|
||||
# Find a node that already has telemetry so the index relationship is
|
||||
# easy to assert on the other side.
|
||||
edit_idx = None
|
||||
for i, raw in enumerate(lines[1:], start=1):
|
||||
node = json.loads(raw)
|
||||
if node.get("telemetry") is not None:
|
||||
edit_idx = i
|
||||
break
|
||||
assert edit_idx is not None, "expected at least one node with telemetry"
|
||||
|
||||
node = json.loads(lines[edit_idx])
|
||||
target_num = int(node["num"], 16)
|
||||
node["long_name"] = "Hand Edited Node"
|
||||
node["telemetry"] = {
|
||||
"battery_level": 42,
|
||||
"voltage": 3.71,
|
||||
"channel_utilization": 0.0,
|
||||
"air_util_tx": 0.0,
|
||||
"uptime_seconds": 1,
|
||||
}
|
||||
lines[edit_idx] = json.dumps(node, ensure_ascii=False, sort_keys=True)
|
||||
dst.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
out = tmp_path / "out.proto"
|
||||
_run([sys.executable, str(COMPILE), "--in", str(dst), "--out", str(out)])
|
||||
db = NodeDatabase()
|
||||
db.ParseFromString(out.read_bytes())
|
||||
edited = next((n for n in db.nodes if n.num == target_num), None)
|
||||
assert edited is not None
|
||||
assert edited.long_name == "Hand Edited Node"
|
||||
tel = next((t for t in db.telemetry if t.num == target_num), None)
|
||||
assert tel is not None
|
||||
assert tel.device_metrics.battery_level == 42
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Misc smoke checks on the module surface.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_crc16_ccitt_matches_known_vectors() -> None:
|
||||
"""Sanity-check the hand-rolled CRC16-CCITT matches well-known vectors.
|
||||
|
||||
Test vectors from the XModem-CRC spec (init=0, poly=0x1021):
|
||||
crc16("123456789") = 0x31C3
|
||||
crc16("") = 0x0000
|
||||
"""
|
||||
from meshtastic_mcp.fixtures import _crc16_ccitt
|
||||
|
||||
assert _crc16_ccitt(b"") == 0x0000
|
||||
assert _crc16_ccitt(b"123456789") == 0x31C3
|
||||
|
||||
|
||||
def test_push_fake_nodedb_rejects_invalid_size() -> None:
|
||||
from meshtastic_mcp.fixtures import FixtureError, push_fake_nodedb
|
||||
|
||||
with pytest.raises(FixtureError, match="size must be one of"):
|
||||
push_fake_nodedb(size=999, target="portduino") # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_push_fake_nodedb_hardware_requires_confirm() -> None:
|
||||
from meshtastic_mcp.fixtures import FixtureError, push_fake_nodedb
|
||||
|
||||
with pytest.raises(FixtureError, match="confirm=True"):
|
||||
push_fake_nodedb(size=250, target="hardware", port="/dev/cu.fake")
|
||||
|
||||
|
||||
def test_push_fake_nodedb_hardware_requires_port() -> None:
|
||||
from meshtastic_mcp.fixtures import FixtureError, push_fake_nodedb
|
||||
|
||||
with pytest.raises(FixtureError, match="requires a port"):
|
||||
push_fake_nodedb(size=250, target="hardware", confirm=True)
|
||||
|
||||
|
||||
def test_push_fake_nodedb_hardware_rejects_tcp_port() -> None:
|
||||
from meshtastic_mcp.fixtures import FixtureError, push_fake_nodedb
|
||||
|
||||
with pytest.raises(FixtureError, match="not supported"):
|
||||
push_fake_nodedb(
|
||||
size=250, target="hardware", confirm=True, port="tcp://localhost:4403"
|
||||
)
|
||||
+6
-4
@@ -49,6 +49,7 @@ build_flags = -Wno-missing-field-initializers
|
||||
-DRADIOLIB_EXCLUDE_PAGER=1
|
||||
-DRADIOLIB_EXCLUDE_FSK4=1
|
||||
-DRADIOLIB_EXCLUDE_APRS=1
|
||||
-DRADIOLIB_EXCLUDE_ADSB=1
|
||||
-DRADIOLIB_EXCLUDE_LORAWAN=1
|
||||
-DMESHTASTIC_EXCLUDE_DROPZONE=1
|
||||
-DMESHTASTIC_EXCLUDE_REPLYBOT=1
|
||||
@@ -57,7 +58,6 @@ build_flags = -Wno-missing-field-initializers
|
||||
-DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware
|
||||
-DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1
|
||||
-DMESHTASTIC_EXCLUDE_POWERMON=1
|
||||
-DMESHTASTIC_EXCLUDE_STATUS=1
|
||||
-D MAX_THREADS=40 ; As we've split modules, we have more threads to manage
|
||||
#-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now
|
||||
#-D OLED_PL=1
|
||||
@@ -121,7 +121,7 @@ lib_deps =
|
||||
[radiolib_base]
|
||||
lib_deps =
|
||||
# renovate: datasource=github-tags depName=RadioLib packageName=jgromes/RadioLib
|
||||
https://github.com/jgromes/RadioLib/archive/refs/tags/7.6.0.zip
|
||||
https://github.com/jgromes/RadioLib/archive/afe72ae46a343e15e3cac7f26ac585c7f98bffe5.zip
|
||||
|
||||
[device-ui_base]
|
||||
lib_deps =
|
||||
@@ -171,8 +171,8 @@ lib_deps =
|
||||
https://github.com/EmotiBit/EmotiBit_MLX90632/archive/refs/tags/v1.0.8.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit MLX90614 packageName=adafruit/Adafruit_MLX90614
|
||||
https://github.com/adafruit/Adafruit-MLX90614-Library/archive/refs/tags/2.1.6.zip
|
||||
# renovate: datasource=git-refs depName=INA3221 packageName=https://github.com/sgtwilko/INA3221 gitBranch=FixOverflow
|
||||
https://github.com/sgtwilko/INA3221/archive/bb03d7e9bfcc74fc798838a54f4f99738f29fc6a.zip
|
||||
# renovate: datasource=github-tags depName=INA3221_RT packageName=RobTillaart/INA3221_RT
|
||||
https://github.com/RobTillaart/INA3221_RT/archive/refs/tags/0.4.2.zip
|
||||
# renovate: datasource=github-tags depName=QMC5883L Compass packageName=mprograms/QMC5883LCompass
|
||||
https://github.com/mprograms/QMC5883LCompass/archive/refs/tags/v1.2.3.zip
|
||||
# renovate: datasource=github-tags depName=DFRobot_RTU packageName=dfrobot/DFRobot_RTU
|
||||
@@ -227,6 +227,8 @@ lib_deps =
|
||||
https://github.com/Sensirion/arduino-i2c-sfa3x/archive/refs/tags/1.0.0.zip
|
||||
# renovate: datasource=github-tags depName=Sensirion I2C SCD30 packageName=sensirion/arduino-i2c-scd30
|
||||
https://github.com/Sensirion/arduino-i2c-scd30/archive/refs/tags/1.0.0.zip
|
||||
# renovate: datasource=github-tags depName=arduino-sht packageName=sensirion/arduino-sht
|
||||
https://github.com/Sensirion/arduino-sht/archive/refs/tags/v1.2.6.zip
|
||||
|
||||
; Environmental sensors with BSEC2 (Bosch proprietary IAQ)
|
||||
[environmental_extra]
|
||||
|
||||
+1
-1
Submodule protobufs updated: b302d92332...7ffb4bb60d
@@ -4,7 +4,8 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC
|
||||
bool usePreset)
|
||||
{
|
||||
|
||||
// If use_preset is false, always return "Custom"
|
||||
// If use_preset is false, always return "Custom" — callers such as RadioInterface and Channels
|
||||
// rely on this being a stable literal for channel-name hashing and default-channel detection.
|
||||
if (!usePreset) {
|
||||
return "Custom";
|
||||
}
|
||||
|
||||
+64
-94
@@ -79,28 +79,46 @@ bool copyFile(const char *from, const char *to)
|
||||
bool renameFile(const char *pathFrom, const char *pathTo)
|
||||
{
|
||||
#ifdef FSCom
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// take SPI Lock
|
||||
spiLock->lock();
|
||||
// rename was fixed for ESP32 IDF LittleFS in April
|
||||
bool result = FSCom.rename(pathFrom, pathTo);
|
||||
spiLock->unlock();
|
||||
return result;
|
||||
#else
|
||||
// copyFile does its own locking.
|
||||
if (copyFile(pathFrom, pathTo) && FSCom.remove(pathFrom)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* @brief Platform-agnostic filesystem format / wipe.
|
||||
*
|
||||
* On embedded targets (ESP32, NRF52, STM32WL, RP2040) this calls the
|
||||
* native FSCom.format() which erases and reinitialises the LittleFS
|
||||
* partition.
|
||||
*
|
||||
* On Portduino the fs::FS backend has no format() method. We instead
|
||||
* delete /prefs (the only meshtastic data directory written at runtime)
|
||||
* and return. rmDir("/prefs") is already called unconditionally by
|
||||
* factoryReset() so this is a proven primitive on Portduino.
|
||||
* FSBegin() is a no-op (#define FSBegin() true) on Portduino.
|
||||
*
|
||||
* @return true on success, false on failure or if no filesystem is configured.
|
||||
*/
|
||||
bool fsFormat()
|
||||
{
|
||||
#ifdef FSCom
|
||||
#if defined(ARCH_PORTDUINO)
|
||||
rmDir("/prefs");
|
||||
return FSBegin();
|
||||
#else
|
||||
return FSCom.format();
|
||||
#endif
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the list of files in a directory.
|
||||
*
|
||||
@@ -123,23 +141,21 @@ std::vector<meshtastic_FileInfo> getFiles(const char *dirname, uint8_t levels)
|
||||
|
||||
File file = root.openNextFile();
|
||||
while (file) {
|
||||
#ifdef ARCH_ESP32
|
||||
const char *filepath = file.path();
|
||||
#else
|
||||
const char *filepath = file.name();
|
||||
#endif
|
||||
if (file.isDirectory() && !String(file.name()).endsWith(".")) {
|
||||
if (levels) {
|
||||
#ifdef ARCH_ESP32
|
||||
std::vector<meshtastic_FileInfo> subDirFilenames = getFiles(file.path(), levels - 1);
|
||||
#else
|
||||
std::vector<meshtastic_FileInfo> subDirFilenames = getFiles(file.name(), levels - 1);
|
||||
#endif
|
||||
std::vector<meshtastic_FileInfo> subDirFilenames = getFiles(filepath, levels - 1);
|
||||
filenames.insert(filenames.end(), subDirFilenames.begin(), subDirFilenames.end());
|
||||
file.close();
|
||||
}
|
||||
} else {
|
||||
meshtastic_FileInfo fileInfo = {"", static_cast<uint32_t>(file.size())};
|
||||
#ifdef ARCH_ESP32
|
||||
strcpy(fileInfo.file_name, file.path());
|
||||
#else
|
||||
strcpy(fileInfo.file_name, file.name());
|
||||
#endif
|
||||
strncpy(fileInfo.file_name, filepath, sizeof(fileInfo.file_name) - 1);
|
||||
fileInfo.file_name[sizeof(fileInfo.file_name) - 1] = '\0';
|
||||
if (!String(fileInfo.file_name).endsWith(".")) {
|
||||
filenames.push_back(fileInfo);
|
||||
}
|
||||
@@ -163,98 +179,59 @@ std::vector<meshtastic_FileInfo> getFiles(const char *dirname, uint8_t levels)
|
||||
void listDir(const char *dirname, uint8_t levels, bool del)
|
||||
{
|
||||
#ifdef FSCom
|
||||
#if (defined(ARCH_ESP32) || defined(ARCH_RP2040) || defined(ARCH_PORTDUINO))
|
||||
char buffer[255];
|
||||
#endif
|
||||
File root = FSCom.open(dirname, FILE_O_READ);
|
||||
if (!root) {
|
||||
if (!root || !root.isDirectory())
|
||||
return;
|
||||
}
|
||||
if (!root.isDirectory()) {
|
||||
return;
|
||||
}
|
||||
|
||||
File file = root.openNextFile();
|
||||
while (
|
||||
file &&
|
||||
file.name()[0]) { // This file.name() check is a workaround for a bug in the Adafruit LittleFS nrf52 glue (see issue 4395)
|
||||
while (file && file.name()[0]) { // file.name()[0] check: workaround for Adafruit LittleFS nRF52 bug #4395
|
||||
#ifdef ARCH_ESP32
|
||||
const char *filepath = file.path();
|
||||
#else
|
||||
const char *filepath = file.name();
|
||||
#endif
|
||||
if (file.isDirectory() && !String(file.name()).endsWith(".")) {
|
||||
if (levels) {
|
||||
#ifdef ARCH_ESP32
|
||||
listDir(file.path(), levels - 1, del);
|
||||
listDir(filepath, levels - 1, del);
|
||||
if (del) {
|
||||
LOG_DEBUG("Remove %s", file.path());
|
||||
strncpy(buffer, file.path(), sizeof(buffer));
|
||||
LOG_DEBUG("Remove %s", filepath);
|
||||
strncpy(buffer, filepath, sizeof(buffer) - 1);
|
||||
buffer[sizeof(buffer) - 1] = '\0';
|
||||
file.close();
|
||||
FSCom.rmdir(buffer);
|
||||
} else {
|
||||
file.close();
|
||||
}
|
||||
#elif (defined(ARCH_RP2040) || defined(ARCH_PORTDUINO))
|
||||
listDir(file.name(), levels - 1, del);
|
||||
if (del) {
|
||||
LOG_DEBUG("Remove %s", file.name());
|
||||
strncpy(buffer, file.name(), sizeof(buffer));
|
||||
file.close();
|
||||
FSCom.rmdir(buffer);
|
||||
} else {
|
||||
file.close();
|
||||
}
|
||||
#else
|
||||
LOG_DEBUG(" %s (directory)", file.name());
|
||||
listDir(file.name(), levels - 1, del);
|
||||
file.close();
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
#ifdef ARCH_ESP32
|
||||
if (del) {
|
||||
LOG_DEBUG("Delete %s", file.path());
|
||||
strncpy(buffer, file.path(), sizeof(buffer));
|
||||
LOG_DEBUG("Delete %s", filepath);
|
||||
strncpy(buffer, filepath, sizeof(buffer) - 1);
|
||||
buffer[sizeof(buffer) - 1] = '\0';
|
||||
file.close();
|
||||
FSCom.remove(buffer);
|
||||
} else {
|
||||
LOG_DEBUG(" %s (%i Bytes)", file.path(), file.size());
|
||||
LOG_DEBUG(" %s (%i Bytes)", filepath, file.size());
|
||||
file.close();
|
||||
}
|
||||
#elif (defined(ARCH_RP2040) || defined(ARCH_PORTDUINO))
|
||||
if (del) {
|
||||
LOG_DEBUG("Delete %s", file.name());
|
||||
strncpy(buffer, file.name(), sizeof(buffer));
|
||||
file.close();
|
||||
FSCom.remove(buffer);
|
||||
} else {
|
||||
LOG_DEBUG(" %s (%i Bytes)", file.name(), file.size());
|
||||
file.close();
|
||||
}
|
||||
#else
|
||||
LOG_DEBUG(" %s (%i Bytes)", file.name(), file.size());
|
||||
file.close();
|
||||
#endif
|
||||
}
|
||||
file = root.openNextFile();
|
||||
}
|
||||
#ifdef ARCH_ESP32
|
||||
if (del) {
|
||||
LOG_DEBUG("Remove %s", root.path());
|
||||
strncpy(buffer, root.path(), sizeof(buffer));
|
||||
root.close();
|
||||
FSCom.rmdir(buffer);
|
||||
} else {
|
||||
root.close();
|
||||
}
|
||||
#elif (defined(ARCH_RP2040) || defined(ARCH_PORTDUINO))
|
||||
if (del) {
|
||||
LOG_DEBUG("Remove %s", root.name());
|
||||
strncpy(buffer, root.name(), sizeof(buffer));
|
||||
root.close();
|
||||
FSCom.rmdir(buffer);
|
||||
} else {
|
||||
root.close();
|
||||
}
|
||||
const char *rootpath = root.path();
|
||||
#else
|
||||
root.close();
|
||||
const char *rootpath = root.name();
|
||||
#endif
|
||||
if (del) {
|
||||
LOG_DEBUG("Remove %s", rootpath);
|
||||
strncpy(buffer, rootpath, sizeof(buffer) - 1);
|
||||
buffer[sizeof(buffer) - 1] = '\0';
|
||||
root.close();
|
||||
FSCom.rmdir(buffer);
|
||||
} else {
|
||||
root.close();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -268,14 +245,7 @@ void listDir(const char *dirname, uint8_t levels, bool del)
|
||||
void rmDir(const char *dirname)
|
||||
{
|
||||
#ifdef FSCom
|
||||
|
||||
#if (defined(ARCH_ESP32) || defined(ARCH_RP2040) || defined(ARCH_PORTDUINO))
|
||||
listDir(dirname, 10, true);
|
||||
#elif defined(ARCH_NRF52)
|
||||
// nRF52 implementation of LittleFS has a recursive delete function
|
||||
FSCom.rmdir_r(dirname);
|
||||
#endif
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ void fsInit();
|
||||
void fsListFiles();
|
||||
bool copyFile(const char *from, const char *to);
|
||||
bool renameFile(const char *pathFrom, const char *pathTo);
|
||||
bool fsFormat();
|
||||
std::vector<meshtastic_FileInfo> getFiles(const char *dirname, uint8_t levels);
|
||||
void listDir(const char *dirname, uint8_t levels, bool del = false);
|
||||
void rmDir(const char *dirname);
|
||||
|
||||
+3
-6
@@ -53,8 +53,7 @@ class GPSStatus : public Status
|
||||
int32_t getLatitude() const
|
||||
{
|
||||
if (config.position.fixed_position) {
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
return node->position.latitude_i;
|
||||
return localPosition.latitude_i;
|
||||
} else {
|
||||
return p.latitude_i;
|
||||
}
|
||||
@@ -63,8 +62,7 @@ class GPSStatus : public Status
|
||||
int32_t getLongitude() const
|
||||
{
|
||||
if (config.position.fixed_position) {
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
return node->position.longitude_i;
|
||||
return localPosition.longitude_i;
|
||||
} else {
|
||||
return p.longitude_i;
|
||||
}
|
||||
@@ -73,8 +71,7 @@ class GPSStatus : public Status
|
||||
int32_t getAltitude() const
|
||||
{
|
||||
if (config.position.fixed_position) {
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
return node->position.altitude;
|
||||
return localPosition.altitude;
|
||||
} else {
|
||||
return p.altitude;
|
||||
}
|
||||
|
||||
+26
-31
@@ -6,7 +6,6 @@
|
||||
#include "SPILock.h"
|
||||
#include "SafeFile.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "graphics/draw/MessageRenderer.h"
|
||||
#include <cstring> // memcpy
|
||||
|
||||
#ifndef MESSAGE_TEXT_POOL_SIZE
|
||||
@@ -181,13 +180,8 @@ const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &pa
|
||||
|
||||
bool isDM = (sm.dest != 0 && sm.dest != NODENUM_BROADCAST);
|
||||
|
||||
if (packet.from == 0) {
|
||||
sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST;
|
||||
sm.ackStatus = AckStatus::NONE;
|
||||
} else {
|
||||
sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST;
|
||||
sm.ackStatus = AckStatus::ACKED;
|
||||
}
|
||||
sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST;
|
||||
sm.ackStatus = (packet.from == 0) ? AckStatus::NONE : AckStatus::ACKED;
|
||||
|
||||
addLiveMessage(sm);
|
||||
|
||||
@@ -372,26 +366,25 @@ void MessageStore::clearAllMessages()
|
||||
#endif
|
||||
}
|
||||
|
||||
// Internal helper: erase first or last message matching a predicate
|
||||
template <typename Predicate> static void eraseIf(std::deque<StoredMessage> &deque, Predicate pred, bool fromBack = false)
|
||||
// Internal helpers for targeted erasure.
|
||||
template <typename Predicate> static bool eraseFirstMatch(std::deque<StoredMessage> &deque, Predicate pred)
|
||||
{
|
||||
if (fromBack) {
|
||||
// Iterate from the back and erase all matches from the end
|
||||
for (auto it = deque.rbegin(); it != deque.rend();) {
|
||||
if (pred(*it)) {
|
||||
it = std::deque<StoredMessage>::reverse_iterator(deque.erase(std::next(it).base()));
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
for (auto it = deque.begin(); it != deque.end(); ++it) {
|
||||
if (pred(*it)) {
|
||||
deque.erase(it);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// Manual forward search to erase all matches
|
||||
for (auto it = deque.begin(); it != deque.end();) {
|
||||
if (pred(*it)) {
|
||||
it = deque.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
template <typename Predicate> static void eraseAllMatches(std::deque<StoredMessage> &deque, Predicate pred)
|
||||
{
|
||||
for (auto it = deque.begin(); it != deque.end();) {
|
||||
if (pred(*it)) {
|
||||
it = deque.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -399,7 +392,9 @@ template <typename Predicate> static void eraseIf(std::deque<StoredMessage> &deq
|
||||
// Delete oldest message (RAM + persisted queue)
|
||||
void MessageStore::deleteOldestMessage()
|
||||
{
|
||||
eraseIf(liveMessages, [](StoredMessage &) { return true; });
|
||||
if (!liveMessages.empty()) {
|
||||
liveMessages.pop_front();
|
||||
}
|
||||
saveToFlash();
|
||||
}
|
||||
|
||||
@@ -407,14 +402,14 @@ void MessageStore::deleteOldestMessage()
|
||||
void MessageStore::deleteOldestMessageInChannel(uint8_t channel)
|
||||
{
|
||||
auto pred = [channel](const StoredMessage &m) { return m.type == MessageType::BROADCAST && m.channelIndex == channel; };
|
||||
eraseIf(liveMessages, pred);
|
||||
eraseFirstMatch(liveMessages, pred);
|
||||
saveToFlash();
|
||||
}
|
||||
|
||||
void MessageStore::deleteAllMessagesInChannel(uint8_t channel)
|
||||
{
|
||||
auto pred = [channel](const StoredMessage &m) { return m.type == MessageType::BROADCAST && m.channelIndex == channel; };
|
||||
eraseIf(liveMessages, pred, false /* delete ALL, not just first */);
|
||||
eraseAllMatches(liveMessages, pred);
|
||||
saveToFlash();
|
||||
}
|
||||
|
||||
@@ -427,7 +422,7 @@ void MessageStore::deleteAllMessagesWithPeer(uint32_t peer)
|
||||
uint32_t other = (m.sender == local) ? m.dest : m.sender;
|
||||
return other == peer;
|
||||
};
|
||||
eraseIf(liveMessages, pred, false);
|
||||
eraseAllMatches(liveMessages, pred);
|
||||
saveToFlash();
|
||||
}
|
||||
|
||||
@@ -440,7 +435,7 @@ void MessageStore::deleteOldestMessageWithPeer(uint32_t peer)
|
||||
uint32_t other = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender;
|
||||
return other == peer;
|
||||
};
|
||||
eraseIf(liveMessages, pred);
|
||||
eraseFirstMatch(liveMessages, pred);
|
||||
saveToFlash();
|
||||
}
|
||||
|
||||
|
||||
@@ -124,9 +124,6 @@ class MessageStore
|
||||
// Allocate text into pool (used by sender-side code)
|
||||
static uint16_t storeText(const char *src, size_t len);
|
||||
|
||||
// Used when loading from flash to rebuild the text pool
|
||||
static uint16_t rebuildTextFromFlash(const char *src, size_t len);
|
||||
|
||||
private:
|
||||
std::deque<StoredMessage> liveMessages; // Single in-RAM message buffer (also used for persistence)
|
||||
std::string filename; // Flash filename for persistence
|
||||
|
||||
+82
-58
@@ -40,6 +40,22 @@
|
||||
#include "concurrency/LockGuard.h"
|
||||
#endif
|
||||
|
||||
#if defined(ARCH_STM32WL) && defined(BATTERY_PIN)
|
||||
#include "stm32yyxx_ll_adc.h"
|
||||
|
||||
/* Analog read resolution */
|
||||
#if defined(LL_ADC_RESOLUTION_12B)
|
||||
#define LL_ADC_RESOLUTION LL_ADC_RESOLUTION_12B
|
||||
#define BATTERY_SENSE_RESOLUTION_BITS 12
|
||||
#elif defined(LL_ADC_DS_DATA_WIDTH_12_BIT)
|
||||
#define LL_ADC_RESOLUTION LL_ADC_DS_DATA_WIDTH_12_BIT
|
||||
#define BATTERY_SENSE_RESOLUTION_BITS 12
|
||||
#else
|
||||
#error "ADC resolution could not be defined!"
|
||||
#endif
|
||||
#define ADC_RANGE (1 << BATTERY_SENSE_RESOLUTION_BITS)
|
||||
#endif
|
||||
|
||||
#if defined(DEBUG_HEAP_MQTT) && !MESHTASTIC_EXCLUDE_MQTT
|
||||
#include "mqtt/MQTT.h"
|
||||
#include "target_specific.h"
|
||||
@@ -78,16 +94,31 @@ static const adc_atten_t atten = ADC_ATTENUATION;
|
||||
#endif
|
||||
#endif // BATTERY_PIN && ARCH_ESP32
|
||||
|
||||
#ifdef EXT_PWR_DETECT
|
||||
#ifndef EXT_PWR_DETECT_MODE
|
||||
#define EXT_PWR_DETECT_MODE INPUT
|
||||
// If using internal pull resistors, we can infer EXT_PWR_DETECT_VALUE
|
||||
#elif EXT_PWR_DETECT_MODE == INPUT_PULLUP
|
||||
#define EXT_PWR_DETECT_VALUE LOW
|
||||
#elif EXT_PWR_DETECT_MODE == INPUT_PULLDOWN
|
||||
#define EXT_PWR_DETECT_VALUE HIGH
|
||||
#endif
|
||||
#ifndef EXT_PWR_DETECT_VALUE
|
||||
#define EXT_PWR_DETECT_VALUE HIGH
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef EXT_CHRG_DETECT
|
||||
#ifndef EXT_CHRG_DETECT_MODE
|
||||
static const uint8_t ext_chrg_detect_mode = INPUT;
|
||||
#else
|
||||
static const uint8_t ext_chrg_detect_mode = EXT_CHRG_DETECT_MODE;
|
||||
#define EXT_CHRG_DETECT_MODE INPUT
|
||||
// If using internal pull resistors, we can infer EXT_CHRG_DETECT_VALUE
|
||||
#elif EXT_CHRG_DETECT_MODE == INPUT_PULLUP
|
||||
#define EXT_CHRG_DETECT_VALUE LOW
|
||||
#elif EXT_CHRG_DETECT_MODE == INPUT_PULLDOWN
|
||||
#define EXT_CHRG_DETECT_VALUE HIGH
|
||||
#endif
|
||||
#ifndef EXT_CHRG_DETECT_VALUE
|
||||
static const uint8_t ext_chrg_detect_value = HIGH;
|
||||
#else
|
||||
static const uint8_t ext_chrg_detect_value = EXT_CHRG_DETECT_VALUE;
|
||||
#define EXT_CHRG_DETECT_VALUE HIGH
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -328,11 +359,17 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
float scaled = 0;
|
||||
|
||||
battery_adcEnable();
|
||||
#ifdef ARCH_ESP32 // ADC block for espressif platforms
|
||||
#ifdef ARCH_STM32WL
|
||||
// STM32 ADC with VREFINT runtime calibration
|
||||
Vref = __LL_ADC_CALC_VREFANALOG_VOLTAGE(analogRead(AVREF), LL_ADC_RESOLUTION);
|
||||
raw = analogRead(BATTERY_PIN);
|
||||
scaled = __LL_ADC_CALC_DATA_TO_VOLTAGE(Vref, raw, LL_ADC_RESOLUTION);
|
||||
scaled *= operativeAdcMultiplier;
|
||||
#elif defined(ARCH_ESP32) // ADC block for espressif platforms
|
||||
raw = espAdcRead();
|
||||
scaled = esp_adc_cal_raw_to_voltage(raw, adc_characs);
|
||||
scaled *= operativeAdcMultiplier;
|
||||
#else // block for all other platforms
|
||||
#else // block for all other platforms
|
||||
#ifdef ARCH_NRF52
|
||||
concurrency::LockGuard saadcGuard(concurrency::nrf52SaadcLock);
|
||||
#endif
|
||||
@@ -447,28 +484,14 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
virtual bool isBatteryConnect() override { return getBatteryPercent() != -1; }
|
||||
#endif
|
||||
|
||||
/// If we see a battery voltage higher than physics allows - assume charger is
|
||||
/// pumping in power On some boards we don't have the power management chip
|
||||
/// (like AXPxxxx) so we use EXT_PWR_DETECT GPIO pin to detect external power
|
||||
/// source
|
||||
// Detect if an external power source is connected if we don’t have a PMIC;
|
||||
// Firstly prefer EXT_PWR_DETECT GPIO if available,
|
||||
// secondly try an nRF52-specific routine on some variants,
|
||||
// lastly provide a fallback to indicate external power when fully charged.
|
||||
virtual bool isVbusIn() override
|
||||
{
|
||||
#ifdef EXT_PWR_DETECT
|
||||
#if defined(HELTEC_CAPSULE_SENSOR_V3) || defined(HELTEC_SENSOR_HUB)
|
||||
// if external powered that pin will be pulled down
|
||||
if (digitalRead(EXT_PWR_DETECT) == LOW) {
|
||||
return true;
|
||||
}
|
||||
// if it's not LOW - check the battery
|
||||
#else
|
||||
// if external powered that pin will be pulled up
|
||||
if (digitalRead(EXT_PWR_DETECT) == HIGH) {
|
||||
return true;
|
||||
}
|
||||
// if it's not HIGH - check the battery
|
||||
#endif
|
||||
// If we have an EXT_PWR_DETECT pin and it indicates no external power, believe it.
|
||||
return false;
|
||||
return digitalRead(EXT_PWR_DETECT) == EXT_PWR_DETECT_VALUE;
|
||||
|
||||
// technically speaking this should work for all(?) NRF52 boards
|
||||
// but needs testing across multiple devices. NRF52 USB would not even work if
|
||||
@@ -489,9 +512,9 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
}
|
||||
#endif
|
||||
#if defined(ELECROW_ThinkNode_M6)
|
||||
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value || isVbusIn();
|
||||
return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE || isVbusIn();
|
||||
#elif EXT_CHRG_DETECT
|
||||
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value;
|
||||
return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE;
|
||||
#elif defined(BATTERY_CHARGING_INV)
|
||||
return !digitalRead(BATTERY_CHARGING_INV);
|
||||
#else
|
||||
@@ -530,6 +553,11 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
bool initial_read_done = false;
|
||||
float last_read_value = (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS);
|
||||
uint32_t last_read_time_ms = 0;
|
||||
#ifdef ARCH_STM32WL
|
||||
// 3300mV placeholder for STM32 errata where VREFINT factory calibration may be missing
|
||||
// (e.g. STM32U0, see DS14756 Rev 3 §2.4.1 "VREFINT offset")
|
||||
uint32_t Vref = 3300;
|
||||
#endif
|
||||
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT)
|
||||
|
||||
@@ -619,14 +647,10 @@ Power::Power() : OSThread("Power")
|
||||
bool Power::analogInit()
|
||||
{
|
||||
#ifdef EXT_PWR_DETECT
|
||||
#if defined(HELTEC_CAPSULE_SENSOR_V3) || defined(HELTEC_SENSOR_HUB)
|
||||
pinMode(EXT_PWR_DETECT, INPUT_PULLUP);
|
||||
#else
|
||||
pinMode(EXT_PWR_DETECT, INPUT);
|
||||
#endif
|
||||
pinMode(EXT_PWR_DETECT, EXT_PWR_DETECT_MODE);
|
||||
#endif
|
||||
#ifdef EXT_CHRG_DETECT
|
||||
pinMode(EXT_CHRG_DETECT, ext_chrg_detect_mode);
|
||||
pinMode(EXT_CHRG_DETECT, EXT_CHRG_DETECT_MODE);
|
||||
#endif
|
||||
|
||||
#ifdef BATTERY_PIN
|
||||
@@ -639,7 +663,9 @@ bool Power::analogInit()
|
||||
#define BATTERY_SENSE_RESOLUTION_BITS 10
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_ESP32 // ESP32 needs special analog stuff
|
||||
#ifdef ARCH_STM32WL
|
||||
analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS);
|
||||
#elif defined(ARCH_ESP32) // ESP32 needs special analog stuff
|
||||
|
||||
#ifndef ADC_WIDTH // max resolution by default
|
||||
static const adc_bits_width_t width = ADC_WIDTH_BIT_12;
|
||||
@@ -649,7 +675,7 @@ bool Power::analogInit()
|
||||
#ifndef BAT_MEASURE_ADC_UNIT // ADC1
|
||||
adc1_config_width(width);
|
||||
adc1_config_channel_atten(adc_channel, atten);
|
||||
#else // ADC2
|
||||
#else // ADC2
|
||||
adc2_config_channel_atten(adc_channel, atten);
|
||||
#ifndef CONFIG_IDF_TARGET_ESP32S3
|
||||
// ADC2 wifi bug workaround
|
||||
@@ -679,7 +705,7 @@ bool Power::analogInit()
|
||||
|
||||
// NRF52 ADC init moved to powerHAL_init in nrf52 platform
|
||||
|
||||
#ifndef ARCH_ESP32
|
||||
#if !defined(ARCH_ESP32) && !defined(ARCH_STM32WL)
|
||||
analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS);
|
||||
#endif
|
||||
|
||||
@@ -985,6 +1011,14 @@ int32_t Power::runOnce()
|
||||
powerFSM.trigger(EVENT_POWER_CONNECTED);
|
||||
}
|
||||
|
||||
#ifdef PMU_POWER_BUTTON_IS_CANCEL
|
||||
// cancel action also turns the screen on and off.
|
||||
if (PMU->isPekeyShortPressIrq()) {
|
||||
LOG_INFO("Input: Corona Button Click");
|
||||
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_CANCEL, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||
inputBroker->injectInputEvent(&event);
|
||||
}
|
||||
#endif
|
||||
/*
|
||||
Other things we could check if we cared...
|
||||
|
||||
@@ -1001,13 +1035,6 @@ int32_t Power::runOnce()
|
||||
LOG_DEBUG("Battery removed");
|
||||
}
|
||||
*/
|
||||
#ifndef T_WATCH_S3 // FIXME - why is this triggering on the T-Watch S3?
|
||||
if (PMU->isPekeyLongPressIrq()) {
|
||||
LOG_DEBUG("PEK long button press");
|
||||
if (screen)
|
||||
screen->setOn(false);
|
||||
}
|
||||
#endif
|
||||
|
||||
PMU->clearIrqStatus();
|
||||
}
|
||||
@@ -1076,7 +1103,7 @@ void Power::attachPowerInterrupts()
|
||||
if (PMU) {
|
||||
attachInterrupt(
|
||||
PMU_IRQ,
|
||||
[] {
|
||||
[]() {
|
||||
pmu_irq = true;
|
||||
power->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
@@ -1379,19 +1406,16 @@ bool Power::axpChipInit()
|
||||
uint64_t pmuIrqMask = 0;
|
||||
|
||||
if (PMU->getChipModel() == XPOWERS_AXP192) {
|
||||
pmuIrqMask = XPOWERS_AXP192_VBUS_INSERT_IRQ | XPOWERS_AXP192_BAT_INSERT_IRQ | XPOWERS_AXP192_PKEY_SHORT_IRQ;
|
||||
pmuIrqMask = XPOWERS_AXP192_VBUS_INSERT_IRQ | XPOWERS_AXP192_VBUS_REMOVE_IRQ | XPOWERS_AXP192_PKEY_SHORT_IRQ;
|
||||
} else if (PMU->getChipModel() == XPOWERS_AXP2101) {
|
||||
pmuIrqMask = XPOWERS_AXP2101_VBUS_INSERT_IRQ | XPOWERS_AXP2101_BAT_INSERT_IRQ | XPOWERS_AXP2101_PKEY_SHORT_IRQ;
|
||||
pmuIrqMask = XPOWERS_AXP2101_VBUS_INSERT_IRQ | XPOWERS_AXP2101_VBUS_REMOVE_IRQ | XPOWERS_AXP2101_PKEY_SHORT_IRQ;
|
||||
}
|
||||
|
||||
pinMode(PMU_IRQ, INPUT);
|
||||
|
||||
// we do not look for AXPXXX_CHARGING_FINISHED_IRQ & AXPXXX_CHARGING_IRQ
|
||||
// because it occurs repeatedly while there is no battery also it could cause
|
||||
// inadvertent waking from light sleep just because the battery filled we
|
||||
// don't look for AXPXXX_BATT_REMOVED_IRQ because it occurs repeatedly while
|
||||
// no battery installed we don't look at AXPXXX_VBUS_REMOVED_IRQ because we
|
||||
// don't have anything hooked to vbus
|
||||
// We wake on IRQ, so only enable the IRQs that we care about.
|
||||
// we want USB plug and unplug to update the screen and LED status,
|
||||
// and short press on the power button to trigger the "cancel" action in the UI (which also turns the screen on and off).
|
||||
PMU->enableIRQ(pmuIrqMask);
|
||||
|
||||
PMU->clearIrqStatus();
|
||||
@@ -1857,7 +1881,7 @@ class SerialBatteryLevel : public HasBatteryLevel
|
||||
{
|
||||
#if defined(EXT_CHRG_DETECT)
|
||||
|
||||
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value;
|
||||
return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE;
|
||||
|
||||
#endif
|
||||
return false;
|
||||
@@ -1866,7 +1890,7 @@ class SerialBatteryLevel : public HasBatteryLevel
|
||||
virtual bool isCharging() override
|
||||
{
|
||||
#ifdef EXT_CHRG_DETECT
|
||||
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value;
|
||||
return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE;
|
||||
|
||||
#endif
|
||||
// by default, we check the battery voltage only
|
||||
@@ -1888,10 +1912,10 @@ SerialBatteryLevel serialBatteryLevel;
|
||||
bool Power::serialBatteryInit()
|
||||
{
|
||||
#ifdef EXT_PWR_DETECT
|
||||
pinMode(EXT_PWR_DETECT, INPUT);
|
||||
pinMode(EXT_PWR_DETECT, EXT_PWR_DETECT_MODE);
|
||||
#endif
|
||||
#ifdef EXT_CHRG_DETECT
|
||||
pinMode(EXT_CHRG_DETECT, ext_chrg_detect_mode);
|
||||
pinMode(EXT_CHRG_DETECT, EXT_CHRG_DETECT_MODE);
|
||||
#endif
|
||||
|
||||
bool result = serialBatteryLevel.runOnce();
|
||||
|
||||
+48
-11
@@ -58,6 +58,35 @@ static bool isPowered()
|
||||
return !isPowerSavingMode && powerStatus && (!powerStatus->getHasBattery() || powerStatus->getHasUSB());
|
||||
}
|
||||
|
||||
#if defined(T5_S3_EPAPER_PRO)
|
||||
static void t5BacklightOffForSleep()
|
||||
{
|
||||
t5BacklightSetForcedBySleep(true);
|
||||
}
|
||||
|
||||
static void t5BacklightWakeFromSleep()
|
||||
{
|
||||
t5BacklightSetForcedBySleep(false);
|
||||
}
|
||||
|
||||
static void t5BacklightOffForTimeout()
|
||||
{
|
||||
t5BacklightSetForcedByTimeout(true);
|
||||
t5TouchSetForcedByTimeout(true);
|
||||
}
|
||||
|
||||
static void t5BacklightOnFromUserInput()
|
||||
{
|
||||
t5BacklightHandleUserInput();
|
||||
t5TouchHandleUserInput();
|
||||
}
|
||||
#else
|
||||
static void t5BacklightOffForSleep() {}
|
||||
static void t5BacklightWakeFromSleep() {}
|
||||
static void t5BacklightOffForTimeout() {}
|
||||
static void t5BacklightOnFromUserInput() {}
|
||||
#endif
|
||||
|
||||
static void sdsEnter()
|
||||
{
|
||||
LOG_POWERFSM("State: SDS");
|
||||
@@ -87,6 +116,7 @@ static void lsEnter()
|
||||
LOG_POWERFSM("lsEnter begin, ls_secs=%u", config.power.ls_secs);
|
||||
if (screen)
|
||||
screen->setOn(false);
|
||||
t5BacklightOffForSleep();
|
||||
secsSlept = 0; // How long have we been sleeping this time
|
||||
|
||||
// LOG_INFO("lsEnter end");
|
||||
@@ -159,6 +189,8 @@ static void lsIdle()
|
||||
static void lsExit()
|
||||
{
|
||||
LOG_POWERFSM("State: lsExit");
|
||||
// Lift the light-sleep force-off gate when leaving LS.
|
||||
t5BacklightWakeFromSleep();
|
||||
}
|
||||
|
||||
static void nbEnter()
|
||||
@@ -180,6 +212,8 @@ static void darkEnter()
|
||||
setBluetoothEnable(true);
|
||||
if (screen)
|
||||
screen->setOn(false);
|
||||
// Screen timeout enters DARK; ensure backlight also turns off.
|
||||
t5BacklightOffForTimeout();
|
||||
}
|
||||
|
||||
static void serialEnter()
|
||||
@@ -289,12 +323,13 @@ void PowerFSM_setup()
|
||||
powerFSM.add_transition(&stateNB, &stateNB, EVENT_PACKET_FOR_PHONE, NULL, "Received packet, resetting win wake");
|
||||
|
||||
// Handle press events - note: we ignore button presses when in API mode
|
||||
powerFSM.add_transition(&stateLS, &stateON, EVENT_PRESS, NULL, "Press");
|
||||
powerFSM.add_transition(&stateNB, &stateON, EVENT_PRESS, NULL, "Press");
|
||||
powerFSM.add_transition(&stateDARK, isPowered() ? &statePOWER : &stateON, EVENT_PRESS, NULL, "Press");
|
||||
powerFSM.add_transition(&statePOWER, &statePOWER, EVENT_PRESS, NULL, "Press");
|
||||
powerFSM.add_transition(&stateON, &stateON, EVENT_PRESS, NULL, "Press"); // reenter On to restart our timers
|
||||
powerFSM.add_transition(&stateSERIAL, &stateSERIAL, EVENT_PRESS, NULL,
|
||||
powerFSM.add_transition(&stateLS, &stateON, EVENT_PRESS, t5BacklightOnFromUserInput, "Press");
|
||||
powerFSM.add_transition(&stateNB, &stateON, EVENT_PRESS, t5BacklightOnFromUserInput, "Press");
|
||||
powerFSM.add_transition(&stateDARK, isPowered() ? &statePOWER : &stateON, EVENT_PRESS, t5BacklightOnFromUserInput, "Press");
|
||||
powerFSM.add_transition(&statePOWER, &statePOWER, EVENT_PRESS, t5BacklightOnFromUserInput, "Press");
|
||||
powerFSM.add_transition(&stateON, &stateON, EVENT_PRESS, t5BacklightOnFromUserInput,
|
||||
"Press"); // reenter On to restart our timers
|
||||
powerFSM.add_transition(&stateSERIAL, &stateSERIAL, EVENT_PRESS, t5BacklightOnFromUserInput,
|
||||
"Press"); // Allow button to work while in serial API
|
||||
|
||||
// Handle critically low power battery by forcing deep sleep
|
||||
@@ -314,11 +349,13 @@ void PowerFSM_setup()
|
||||
powerFSM.add_transition(&stateSERIAL, &stateSHUTDOWN, EVENT_SHUTDOWN, NULL, "Shutdown");
|
||||
|
||||
// Inputbroker
|
||||
powerFSM.add_transition(&stateLS, &stateON, EVENT_INPUT, NULL, "Input Device");
|
||||
powerFSM.add_transition(&stateNB, &stateON, EVENT_INPUT, NULL, "Input Device");
|
||||
powerFSM.add_transition(&stateDARK, &stateON, EVENT_INPUT, NULL, "Input Device");
|
||||
powerFSM.add_transition(&stateON, &stateON, EVENT_INPUT, NULL, "Input Device"); // restarts the sleep timer
|
||||
powerFSM.add_transition(&statePOWER, &statePOWER, EVENT_INPUT, NULL, "Input Device"); // restarts the sleep timer
|
||||
powerFSM.add_transition(&stateLS, &stateON, EVENT_INPUT, t5BacklightOnFromUserInput, "Input Device");
|
||||
powerFSM.add_transition(&stateNB, &stateON, EVENT_INPUT, t5BacklightOnFromUserInput, "Input Device");
|
||||
powerFSM.add_transition(&stateDARK, &stateON, EVENT_INPUT, t5BacklightOnFromUserInput, "Input Device");
|
||||
powerFSM.add_transition(&stateON, &stateON, EVENT_INPUT, t5BacklightOnFromUserInput,
|
||||
"Input Device"); // restarts the sleep timer
|
||||
powerFSM.add_transition(&statePOWER, &statePOWER, EVENT_INPUT, t5BacklightOnFromUserInput,
|
||||
"Input Device"); // restarts the sleep timer
|
||||
|
||||
powerFSM.add_transition(&stateDARK, &stateON, EVENT_BLUETOOTH_PAIR, NULL, "Bluetooth pairing");
|
||||
powerFSM.add_transition(&stateON, &stateON, EVENT_BLUETOOTH_PAIR, NULL, "Bluetooth pairing");
|
||||
|
||||
@@ -137,7 +137,7 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format,
|
||||
if (color) {
|
||||
::printf("\u001b[0m");
|
||||
}
|
||||
::printf("| %02d:%02d:%02d %u ", hour, min, sec, millis() / 1000);
|
||||
::printf("| %02d:%02d:%02d %u.%03u ", hour, min, sec, millis() / 1000, millis() % 1000);
|
||||
#else
|
||||
printf("%s ", logLevel);
|
||||
if (color) {
|
||||
@@ -151,7 +151,7 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format,
|
||||
if (color) {
|
||||
::printf("\u001b[0m");
|
||||
}
|
||||
::printf("| ??:??:?? %u ", millis() / 1000);
|
||||
::printf("| ??:??:?? %u.%03u ", millis() / 1000, millis() % 1000);
|
||||
#else
|
||||
printf("%s ", logLevel);
|
||||
if (color) {
|
||||
@@ -230,9 +230,9 @@ void RedirectablePrint::log_to_ble(const char *logLevel, const char *format, va_
|
||||
auto thread = concurrency::OSThread::currentThread;
|
||||
meshtastic_LogRecord logRecord = meshtastic_LogRecord_init_zero;
|
||||
logRecord.level = getLogLevel(logLevel);
|
||||
vsprintf(logRecord.message, format, arg);
|
||||
vsnprintf(logRecord.message, sizeof(logRecord.message), format, arg);
|
||||
if (thread)
|
||||
strcpy(logRecord.source, thread->ThreadName.c_str());
|
||||
strlcpy(logRecord.source, thread->ThreadName.c_str(), sizeof(logRecord.source));
|
||||
logRecord.time = getValidTime(RTCQuality::RTCQualityDevice, true);
|
||||
|
||||
auto buffer = std::unique_ptr<uint8_t[]>(new uint8_t[meshtastic_LogRecord_size]);
|
||||
|
||||
@@ -7,10 +7,6 @@ static File openFile(const char *filename, bool fullAtomic)
|
||||
{
|
||||
concurrency::LockGuard g(spiLock);
|
||||
LOG_DEBUG("Opening %s, fullAtomic=%d", filename, fullAtomic);
|
||||
#ifdef ARCH_NRF52
|
||||
FSCom.remove(filename);
|
||||
return FSCom.open(filename, FILE_O_WRITE);
|
||||
#endif
|
||||
if (!fullAtomic) {
|
||||
FSCom.remove(filename); // Nuke the old file to make space (ignore if it !exists)
|
||||
}
|
||||
@@ -67,9 +63,6 @@ bool SafeFile::close()
|
||||
f.close();
|
||||
spiLock->unlock();
|
||||
|
||||
#ifdef ARCH_NRF52
|
||||
return true;
|
||||
#endif
|
||||
if (!testReadback())
|
||||
return false;
|
||||
|
||||
|
||||
@@ -30,6 +30,9 @@ SerialConsole *console;
|
||||
|
||||
void consoleInit()
|
||||
{
|
||||
if (console) {
|
||||
return;
|
||||
}
|
||||
auto sc = new SerialConsole(); // Must be dynamically allocated because we are now inheriting from thread
|
||||
|
||||
#if defined(SERIAL_HAS_ON_RECEIVE)
|
||||
|
||||
+2
-2
@@ -19,8 +19,8 @@
|
||||
|
||||
TX_LOG + RX_LOG = Total air time for a particular meshtastic channel.
|
||||
|
||||
TX_LOG + RX_LOG = Total air time for a particular meshtastic channel, including
|
||||
other lora radios.
|
||||
TX_LOG + RX_ALL_LOG = Total air time for a particular meshtastic channel, including
|
||||
other lora radios.
|
||||
|
||||
RX_ALL_LOG - RX_LOG = Other lora radios on our frequency channel.
|
||||
*/
|
||||
|
||||
+10
-3
@@ -78,6 +78,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
// Configuration
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// Pre-hop drop handling (compile-time flag).
|
||||
#ifndef MESHTASTIC_PREHOP_DROP
|
||||
#define MESHTASTIC_PREHOP_DROP 1
|
||||
#endif
|
||||
|
||||
/// Convert a preprocessor name into a quoted string
|
||||
#define xstr(s) ystr(s)
|
||||
#define ystr(s) #s
|
||||
@@ -229,7 +234,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define BME_ADDR 0x76
|
||||
#define BME_ADDR_ALTERNATE 0x77
|
||||
#define MCP9808_ADDR 0x18
|
||||
#define INA_ADDR 0x40
|
||||
#define INA_ADDR 0x40 // same as SHT2X
|
||||
#define INA_ADDR_ALTERNATE 0x41
|
||||
#define INA_ADDR_WAVESHARE_UPS 0x43
|
||||
#define INA3221_ADDR 0x42
|
||||
@@ -242,8 +247,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define LPS22HB_ADDR 0x5C
|
||||
#define LPS22HB_ADDR_ALT 0x5D
|
||||
#define SFA30_ADDR 0x5D
|
||||
#define SHT31_4x_ADDR 0x44
|
||||
#define SHT31_4x_ADDR_ALT 0x45
|
||||
#define SHTXX_ADDR 0x44
|
||||
#define SHTXX_ADDR_ALT 0x45
|
||||
#define PMSA003I_ADDR 0x12
|
||||
#define QMA6100P_ADDR 0x12
|
||||
#define AHT10_ADDR 0x38
|
||||
@@ -497,6 +502,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define MESHTASTIC_EXCLUDE_PKI 1
|
||||
#define MESHTASTIC_EXCLUDE_POWER_FSM 1
|
||||
#define MESHTASTIC_EXCLUDE_TZ 1
|
||||
#define MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH 1
|
||||
#endif
|
||||
|
||||
// Turn off all optional modules
|
||||
@@ -513,6 +519,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define MESHTASTIC_EXCLUDE_REMOTEHARDWARE 1
|
||||
#define MESHTASTIC_EXCLUDE_STOREFORWARD 1
|
||||
#define MESHTASTIC_EXCLUDE_TEXTMESSAGE 1
|
||||
#define MESHTASTIC_EXCLUDE_TRAFFIC_MANAGEMENT 1
|
||||
#define MESHTASTIC_EXCLUDE_ATAK 1
|
||||
#define MESHTASTIC_EXCLUDE_CANNEDMESSAGES 1
|
||||
#define MESHTASTIC_EXCLUDE_NEIGHBORINFO 1
|
||||
|
||||
@@ -11,7 +11,8 @@ enum LoRaRadioType {
|
||||
SX1280_RADIO,
|
||||
LR1110_RADIO,
|
||||
LR1120_RADIO,
|
||||
LR1121_RADIO
|
||||
LR1121_RADIO,
|
||||
LR2021_RADIO
|
||||
};
|
||||
|
||||
extern LoRaRadioType radioType;
|
||||
@@ -136,7 +136,9 @@ bool ScanI2CTwoWire::i2cCommandResponseLength(ScanI2C::DeviceAddress addr, uint1
|
||||
return match;
|
||||
}
|
||||
|
||||
/// for SEN5X detection
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
|
||||
// FIXME Move to a separate file for detection of sensors that require more complex interactions?
|
||||
// For SEN5X detection
|
||||
// Note, this code needs to be called before setting the I2C bus speed
|
||||
// for the screen at high speed. The speed needs to be at 100kHz, otherwise
|
||||
// detection will not work
|
||||
@@ -174,6 +176,46 @@ String readSEN5xProductName(TwoWire *i2cBus, uint8_t address)
|
||||
|
||||
return String(productName);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||
bool detectSHT21SerialNumber(TwoWire *i2cBus, uint8_t address)
|
||||
{
|
||||
|
||||
i2cBus->beginTransmission(address);
|
||||
i2cBus->write(0xFA);
|
||||
i2cBus->write(0x0F);
|
||||
|
||||
if (i2cBus->endTransmission() != 0)
|
||||
return false;
|
||||
|
||||
if (i2cBus->requestFrom(address, (uint8_t)8) != 8)
|
||||
return false;
|
||||
|
||||
// Just flush the data
|
||||
while (i2cBus->available() < 8) {
|
||||
i2cBus->read();
|
||||
}
|
||||
|
||||
i2cBus->beginTransmission(address);
|
||||
i2cBus->write(0xFC);
|
||||
i2cBus->write(0xC9);
|
||||
|
||||
if (i2cBus->endTransmission() != 0)
|
||||
return false;
|
||||
|
||||
if (i2cBus->requestFrom(address, (uint8_t)6) != 6)
|
||||
return false;
|
||||
|
||||
// Just flush the data
|
||||
while (i2cBus->available() < 6) {
|
||||
i2cBus->read();
|
||||
}
|
||||
|
||||
// Assume we detect the SHT21 if something came back from the request
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#define SCAN_SIMPLE_CASE(ADDR, T, ...) \
|
||||
case ADDR: \
|
||||
@@ -371,27 +413,47 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
break;
|
||||
#endif
|
||||
#if !defined(M5STACK_UNITC6L)
|
||||
case INA_ADDR:
|
||||
case INA_ADDR: // Same as SHT2X
|
||||
case INA_ADDR_ALTERNATE:
|
||||
case INA_ADDR_WAVESHARE_UPS:
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2);
|
||||
LOG_DEBUG("Register MFG_UID: 0x%x", registerValue);
|
||||
if (registerValue == 0x5449) {
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFF), 2);
|
||||
LOG_DEBUG("Register DIE_UID: 0x%x", registerValue);
|
||||
case INA_ADDR_WAVESHARE_UPS: {
|
||||
uint16_t mfg = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2);
|
||||
|
||||
if (registerValue == 0x2260) {
|
||||
LOG_DEBUG("Register MFG_UID: 0x%x", mfg);
|
||||
|
||||
// Only read DIE_UID for vendors we recognize as INA-compatible to avoid
|
||||
// an extra I2C transaction + delay on other devices sharing this address.
|
||||
if (mfg == 0x5449 || mfg == 0x190F) {
|
||||
uint16_t die = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFF), 2);
|
||||
LOG_DEBUG("Register DIE_UID: 0x%x", die);
|
||||
|
||||
// TI INA226 or fully compatible clones (e.g. TPA626)
|
||||
if (mfg == 0x5449 && die == 0x2260) {
|
||||
logFoundDevice("INA226", (uint8_t)addr.address);
|
||||
type = INA226;
|
||||
} else {
|
||||
}
|
||||
// Silergy SQ52201 (INA226-compatible with different IDs)
|
||||
else if (mfg == 0x190F && die == 0x0000) {
|
||||
logFoundDevice("INA226 (SQ52201)", (uint8_t)addr.address);
|
||||
type = INA226;
|
||||
}
|
||||
// TI INA260
|
||||
else if (mfg == 0x5449) {
|
||||
logFoundDevice("INA260", (uint8_t)addr.address);
|
||||
type = INA260;
|
||||
}
|
||||
} else { // Assume INA219 if INA260 ID is not found
|
||||
}
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||
if (type == NONE && detectSHT21SerialNumber(i2cBus, (uint8_t)addr.address)) {
|
||||
logFoundDevice("SHTXX (SHT2X)", (uint8_t)addr.address);
|
||||
type = SHTXX;
|
||||
}
|
||||
#endif
|
||||
else { // Assume INA219 if none of the above ones are found
|
||||
logFoundDevice("INA219", (uint8_t)addr.address);
|
||||
type = INA219;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case INA3221_ADDR:
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2);
|
||||
LOG_DEBUG("Register MFG_UID FE: 0x%x", registerValue);
|
||||
@@ -448,22 +510,19 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SHT31_4x_ADDR: // same as OPT3001_ADDR_ALT
|
||||
case SHT31_4x_ADDR_ALT: // same as OPT3001_ADDR
|
||||
case SHTXX_ADDR: // same as OPT3001_ADDR_ALT
|
||||
case SHTXX_ADDR_ALT: // same as OPT3001_ADDR
|
||||
if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) {
|
||||
type = OPT3001;
|
||||
logFoundDevice("OPT3001", (uint8_t)addr.address);
|
||||
} else if (i2cCommandResponseLength(addr, 0x89, 6)) { // SHT4x serial number (6 bytes inc. CRC)
|
||||
type = SHT4X;
|
||||
logFoundDevice("SHT4X", (uint8_t)addr.address);
|
||||
} else {
|
||||
type = SHT31;
|
||||
logFoundDevice("SHT31", (uint8_t)addr.address);
|
||||
} else { // SHTXX
|
||||
type = SHTXX;
|
||||
logFoundDevice("SHTXX", (uint8_t)addr.address);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
SCAN_SIMPLE_CASE(SHTC3_ADDR, SHTC3, "SHTC3", (uint8_t)addr.address)
|
||||
SCAN_SIMPLE_CASE(SHTC3_ADDR, SHTXX, "SHTXX", (uint8_t)addr.address)
|
||||
case RCWL9620_ADDR:
|
||||
// get MAX30102 PARTID
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFF), 1);
|
||||
@@ -699,6 +758,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
logFoundDevice("BMX160", (uint8_t)addr.address);
|
||||
break;
|
||||
} else {
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
|
||||
String prod = "";
|
||||
prod = readSEN5xProductName(i2cBus, addr.address);
|
||||
if (prod.startsWith("SEN55")) {
|
||||
@@ -714,6 +774,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
logFoundDevice("Sensirion SEN50", addr.address);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
if (addr.address == BMX160_ADDR) {
|
||||
type = BMX160;
|
||||
logFoundDevice("BMX160", (uint8_t)addr.address);
|
||||
|
||||
@@ -103,6 +103,14 @@ static int32_t gpsSwitch()
|
||||
if (gps) {
|
||||
int currentState = digitalRead(PIN_GPS_SWITCH);
|
||||
|
||||
// Respect explicit NOT_PRESENT mode and do not let the hardware switch re-enable GPS.
|
||||
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) {
|
||||
gps->disable();
|
||||
lastState = currentState;
|
||||
firstrun = false;
|
||||
return 1000;
|
||||
}
|
||||
|
||||
// if the switch is set to zero, disable the GPS Thread
|
||||
if (firstrun)
|
||||
if (currentState == LOW)
|
||||
|
||||
+186
-220
@@ -39,6 +39,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#include "draw/NodeListRenderer.h"
|
||||
#include "draw/NotificationRenderer.h"
|
||||
#include "draw/UIRenderer.h"
|
||||
#include "graphics/TFTColorRegions.h"
|
||||
#include "modules/CannedMessageModule.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_GPS
|
||||
@@ -54,26 +55,21 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#include "gps/RTC.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/TFTPalette.h"
|
||||
#include "graphics/emotes.h"
|
||||
#include "graphics/images.h"
|
||||
#include "input/TouchScreenImpl1.h"
|
||||
#include "main.h"
|
||||
#include "mesh-pb-constants.h"
|
||||
#include "mesh/Channels.h"
|
||||
#include "mesh/Default.h"
|
||||
#include "mesh/generated/meshtastic/deviceonly.pb.h"
|
||||
#include "modules/ExternalNotificationModule.h"
|
||||
#include "modules/TextMessageModule.h"
|
||||
#include "modules/WaypointModule.h"
|
||||
#include "sleep.h"
|
||||
#include "target_specific.h"
|
||||
extern MessageStore messageStore;
|
||||
|
||||
#if USE_TFTDISPLAY
|
||||
extern uint16_t TFT_MESH;
|
||||
#else
|
||||
uint16_t TFT_MESH = COLOR565(0x67, 0xEA, 0x94);
|
||||
#endif
|
||||
|
||||
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
|
||||
#include "mesh/wifi/WiFiAPClient.h"
|
||||
#endif
|
||||
@@ -98,6 +94,7 @@ namespace graphics
|
||||
|
||||
// This means the *visible* area (sh1106 can address 132, but shows 128 for example)
|
||||
#define IDLE_FRAMERATE 1 // in fps
|
||||
#define COMPASS_ACTIVE_FRAMERATE 20
|
||||
|
||||
// DEBUG
|
||||
#define NUM_EXTRA_FRAMES 3 // text message and debug frame
|
||||
@@ -107,6 +104,27 @@ namespace graphics
|
||||
// A text message frame + debug frame + all the node infos
|
||||
FrameCallback *normalFrames;
|
||||
static uint32_t targetFramerate = IDLE_FRAMERATE;
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
static inline void prepareFrameColorRegions()
|
||||
{
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
clearTFTColorRegions();
|
||||
// Full-frame FrameMono inversion for themes that need it (e.g. light themes).
|
||||
if (isThemeFullFrameInvert()) {
|
||||
setAndRegisterTFTColorRole(TFTColorRole::FrameMono, getThemeBodyFg(), getThemeBodyBg(), 0, 0, screen->getWidth(),
|
||||
screen->getHeight());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
static inline void updateUiFrame(OLEDDisplayUi *ui)
|
||||
{
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
prepareFrameColorRegions();
|
||||
#endif
|
||||
ui->update();
|
||||
}
|
||||
// Global variables for alert banner - explicitly define with extern "C" linkage to prevent optimization
|
||||
|
||||
uint32_t logo_timeout = 5000; // 4 seconds for EACH logo
|
||||
@@ -135,6 +153,60 @@ static bool heartbeat = false;
|
||||
|
||||
extern bool hasUnreadMessage;
|
||||
|
||||
static inline float wrapHeading360(float heading)
|
||||
{
|
||||
if (heading < 0.0f) {
|
||||
heading += 360.0f;
|
||||
} else if (heading >= 360.0f) {
|
||||
heading -= 360.0f;
|
||||
}
|
||||
return heading;
|
||||
}
|
||||
|
||||
void Screen::setHeading(float heading)
|
||||
{
|
||||
const float wrappedHeading = wrapHeading360(heading);
|
||||
|
||||
if (!hasCompass) {
|
||||
hasCompass = true;
|
||||
compassHeading = wrappedHeading;
|
||||
return;
|
||||
}
|
||||
|
||||
// Interpolate using shortest-path angular delta to avoid jumps around 0/360.
|
||||
float delta = wrappedHeading - compassHeading;
|
||||
if (delta > 180.0f) {
|
||||
delta -= 360.0f;
|
||||
} else if (delta < -180.0f) {
|
||||
delta += 360.0f;
|
||||
}
|
||||
|
||||
// Adaptive filtering:
|
||||
// - Strong damping for tiny deltas (jitter)
|
||||
// - Faster response for larger turns
|
||||
const float absDelta = (delta >= 0.0f) ? delta : -delta;
|
||||
if (absDelta < 1.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
float alpha = 0.35f;
|
||||
if (absDelta > 25.0f) {
|
||||
alpha = 0.85f;
|
||||
} else if (absDelta > 10.0f) {
|
||||
alpha = 0.65f;
|
||||
}
|
||||
|
||||
float step = delta * alpha;
|
||||
const float maxStep = 12.0f;
|
||||
if (step > maxStep) {
|
||||
step = maxStep;
|
||||
} else if (step < -maxStep) {
|
||||
step = -maxStep;
|
||||
}
|
||||
|
||||
compassHeading = wrapHeading360(compassHeading + step);
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// Overlay Alert Banner Renderer
|
||||
// ==============================
|
||||
@@ -158,6 +230,7 @@ void Screen::showOverlayBanner(BannerOverlayOptions banner_overlay_options)
|
||||
#endif
|
||||
// Store the message and set the expiration timestamp
|
||||
strncpy(NotificationRenderer::alertBannerMessage, banner_overlay_options.message, 255);
|
||||
NotificationRenderer::parseBannerMessageWithFonts(NotificationRenderer::alertBannerMessage);
|
||||
NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination
|
||||
NotificationRenderer::alertBannerUntil =
|
||||
(banner_overlay_options.durationMs == 0) ? 0 : millis() + banner_overlay_options.durationMs;
|
||||
@@ -167,11 +240,11 @@ void Screen::showOverlayBanner(BannerOverlayOptions banner_overlay_options)
|
||||
NotificationRenderer::alertBannerCallback = banner_overlay_options.bannerCallback;
|
||||
NotificationRenderer::curSelected = banner_overlay_options.InitialSelected;
|
||||
NotificationRenderer::pauseBanner = false;
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::selection_picker;
|
||||
NotificationRenderer::current_notification_type = banner_overlay_options.notificationType;
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setOverlays(overlays, 2);
|
||||
ui->setTargetFPS(60);
|
||||
ui->update();
|
||||
updateUiFrame(ui);
|
||||
}
|
||||
|
||||
// Called to trigger a banner with custom message and duration
|
||||
@@ -191,9 +264,9 @@ void Screen::showNodePicker(const char *message, uint32_t durationMs, std::funct
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::node_picker;
|
||||
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setOverlays(overlays, 2);
|
||||
ui->setTargetFPS(60);
|
||||
ui->update();
|
||||
updateUiFrame(ui);
|
||||
}
|
||||
|
||||
// Called to trigger a banner with custom message and duration
|
||||
@@ -215,9 +288,9 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
|
||||
NotificationRenderer::currentNumber = 0;
|
||||
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setOverlays(overlays, 2);
|
||||
ui->setTargetFPS(60);
|
||||
ui->update();
|
||||
updateUiFrame(ui);
|
||||
}
|
||||
|
||||
void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||
@@ -238,9 +311,9 @@ void Screen::showTextInput(const char *header, const char *initialText, uint32_t
|
||||
|
||||
// Set the overlay using the same pattern as other notification types
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setOverlays(overlays, 2);
|
||||
ui->setTargetFPS(60);
|
||||
ui->update();
|
||||
updateUiFrame(ui);
|
||||
}
|
||||
|
||||
static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
@@ -272,10 +345,25 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int
|
||||
float Screen::estimatedHeading(double lat, double lon)
|
||||
{
|
||||
static double oldLat, oldLon;
|
||||
static float b;
|
||||
static float b = -1.0f;
|
||||
static uint32_t lastHeadingAtMs = 0;
|
||||
const uint32_t now = millis();
|
||||
const uint32_t gpsUpdateIntervalSecs =
|
||||
Default::getConfiguredOrDefault(config.position.gps_update_interval, default_gps_update_interval);
|
||||
uint32_t effectiveUpdateIntervalSecs = gpsUpdateIntervalSecs;
|
||||
if (config.position.position_broadcast_smart_enabled) {
|
||||
const uint32_t smartMinIntervalSecs = Default::getConfiguredOrDefault(
|
||||
config.position.broadcast_smart_minimum_interval_secs, default_broadcast_smart_minimum_interval_secs);
|
||||
if (smartMinIntervalSecs > effectiveUpdateIntervalSecs) {
|
||||
effectiveUpdateIntervalSecs = smartMinIntervalSecs;
|
||||
}
|
||||
}
|
||||
// Two expected update windows; keep arithmetic 32-bit to avoid pulling in larger 64-bit helpers.
|
||||
const uint32_t headingStaleMs =
|
||||
(effectiveUpdateIntervalSecs > (UINT32_MAX / 2000U)) ? UINT32_MAX : (effectiveUpdateIntervalSecs * 2000U);
|
||||
|
||||
if (oldLat == 0) {
|
||||
// just prepare for next time
|
||||
// Need at least two position points before we can infer heading.
|
||||
oldLat = lat;
|
||||
oldLon = lon;
|
||||
|
||||
@@ -283,12 +371,20 @@ float Screen::estimatedHeading(double lat, double lon)
|
||||
}
|
||||
|
||||
float d = GeoCoord::latLongToMeter(oldLat, oldLon, lat, lon);
|
||||
if (d < 10) // haven't moved enough, just keep current bearing
|
||||
if (d < 10) { // haven't moved enough, keep previous heading (invalid until first real movement)
|
||||
if (lastHeadingAtMs != 0 && (now - lastHeadingAtMs) >= headingStaleMs) {
|
||||
// Heading is stale after prolonged no-movement; force reacquire.
|
||||
b = -1.0f;
|
||||
oldLat = lat;
|
||||
oldLon = lon;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
b = GeoCoord::bearing(oldLat, oldLon, lat, lon) * RAD_TO_DEG;
|
||||
oldLat = lat;
|
||||
oldLon = lon;
|
||||
lastHeadingAtMs = now;
|
||||
|
||||
return b;
|
||||
}
|
||||
@@ -309,30 +405,6 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
|
||||
{
|
||||
graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES];
|
||||
|
||||
int32_t rawRGB = uiconfig.screen_rgb_color;
|
||||
|
||||
// Only validate the combined value once
|
||||
if (rawRGB > 0 && rawRGB <= 255255255) {
|
||||
LOG_INFO("Setting screen RGB color to user chosen: 0x%06X", rawRGB);
|
||||
// Extract each component as a normal int first
|
||||
int r = (rawRGB >> 16) & 0xFF;
|
||||
int g = (rawRGB >> 8) & 0xFF;
|
||||
int b = rawRGB & 0xFF;
|
||||
if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
|
||||
TFT_MESH = COLOR565(static_cast<uint8_t>(r), static_cast<uint8_t>(g), static_cast<uint8_t>(b));
|
||||
}
|
||||
#ifdef TFT_MESH_OVERRIDE
|
||||
} else if (rawRGB == 0) {
|
||||
LOG_INFO("Setting screen RGB color to TFT_MESH_OVERRIDE: 0x%04X", TFT_MESH_OVERRIDE);
|
||||
// Default to TFT_MESH_OVERRIDE if available
|
||||
TFT_MESH = TFT_MESH_OVERRIDE;
|
||||
#endif
|
||||
} else {
|
||||
// Default best readable yellow color
|
||||
LOG_INFO("Setting screen RGB color to default: (255,255,128)");
|
||||
TFT_MESH = COLOR565(255, 255, 128);
|
||||
}
|
||||
|
||||
#if defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SH1107_128_64)
|
||||
dispdev = new SH1106Wire(address.address, -1, -1, geometry,
|
||||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||||
@@ -395,9 +467,13 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
|
||||
#endif
|
||||
|
||||
#if defined(USE_ST7789)
|
||||
static_cast<ST7789Spi *>(dispdev)->setRGB(TFT_MESH);
|
||||
// Keep firmware and ST7789 driver region structs layout-compatible:
|
||||
// we pass `graphics::colorRegions` through a type cast below.
|
||||
static_assert(sizeof(graphics::TFTColorRegion) == sizeof(::TFTColorRegion),
|
||||
"graphics::TFTColorRegion layout must match ST7789 TFTColorRegion");
|
||||
static_cast<ST7789Spi *>(dispdev)->setRGB(TFTPalette::White, (::TFTColorRegion *)colorRegions);
|
||||
#elif defined(USE_ST7796)
|
||||
static_cast<ST7796Spi *>(dispdev)->setRGB(TFT_MESH);
|
||||
static_cast<ST7796Spi *>(dispdev)->setRGB(TFTPalette::White);
|
||||
#endif
|
||||
|
||||
ui = new OLEDDisplayUi(dispdev);
|
||||
@@ -448,6 +524,11 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
||||
delay(100);
|
||||
#endif
|
||||
#if !ARCH_PORTDUINO
|
||||
#if defined(USE_ST7789) && defined(VTFT_CTRL)
|
||||
// Ensure panel power rail is enabled before sending wake commands.
|
||||
pinMode(VTFT_CTRL, OUTPUT);
|
||||
digitalWrite(VTFT_CTRL, LOW);
|
||||
#endif
|
||||
dispdev->displayOn();
|
||||
#endif
|
||||
|
||||
@@ -469,10 +550,6 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
||||
ui->init();
|
||||
#endif
|
||||
#if defined(USE_ST7789) && defined(VTFT_LEDA)
|
||||
#ifdef VTFT_CTRL
|
||||
pinMode(VTFT_CTRL, OUTPUT);
|
||||
digitalWrite(VTFT_CTRL, LOW);
|
||||
#endif
|
||||
ui->init();
|
||||
#ifdef ESP_PLATFORM
|
||||
analogWrite(VTFT_LEDA, BRIGHTNESS_DEFAULT);
|
||||
@@ -513,23 +590,22 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
||||
#endif
|
||||
#ifdef USE_ST7789
|
||||
SPI1.end();
|
||||
#if defined(ARCH_ESP32)
|
||||
// Keep TFT control pins in deterministic states while timed-off.
|
||||
// Floating/default pin states can corrupt panel edge rows on wake.
|
||||
#ifdef VTFT_LEDA
|
||||
pinMode(VTFT_LEDA, ANALOG);
|
||||
pinMode(VTFT_LEDA, OUTPUT);
|
||||
digitalWrite(VTFT_LEDA, !TFT_BACKLIGHT_ON);
|
||||
#endif
|
||||
#ifdef VTFT_CTRL
|
||||
pinMode(VTFT_CTRL, ANALOG);
|
||||
#endif
|
||||
pinMode(ST7789_RESET, ANALOG);
|
||||
pinMode(ST7789_RS, ANALOG);
|
||||
pinMode(ST7789_NSS, ANALOG);
|
||||
#else
|
||||
nrf_gpio_cfg_default(VTFT_LEDA);
|
||||
nrf_gpio_cfg_default(VTFT_CTRL);
|
||||
nrf_gpio_cfg_default(ST7789_RESET);
|
||||
nrf_gpio_cfg_default(ST7789_RS);
|
||||
nrf_gpio_cfg_default(ST7789_NSS);
|
||||
pinMode(VTFT_CTRL, OUTPUT);
|
||||
digitalWrite(VTFT_CTRL, HIGH);
|
||||
#endif
|
||||
pinMode(ST7789_RESET, OUTPUT);
|
||||
digitalWrite(ST7789_RESET, HIGH);
|
||||
pinMode(ST7789_RS, OUTPUT);
|
||||
digitalWrite(ST7789_RS, HIGH);
|
||||
pinMode(ST7789_NSS, OUTPUT);
|
||||
digitalWrite(ST7789_NSS, HIGH);
|
||||
#endif
|
||||
#ifdef USE_ST7796
|
||||
SPI1.end();
|
||||
@@ -584,16 +660,16 @@ void Screen::setup()
|
||||
static_cast<SH1106Wire *>(dispdev)->setSubtype(7);
|
||||
#endif
|
||||
|
||||
#if defined(USE_ST7789) && defined(TFT_MESH)
|
||||
// Apply custom RGB color (e.g. Heltec T114/T190)
|
||||
static_cast<ST7789Spi *>(dispdev)->setRGB(TFT_MESH);
|
||||
#if defined(USE_ST7789)
|
||||
static_assert(sizeof(graphics::TFTColorRegion) == sizeof(::TFTColorRegion),
|
||||
"graphics::TFTColorRegion layout must match ST7789 TFTColorRegion");
|
||||
static_cast<ST7789Spi *>(dispdev)->setRGB(TFTPalette::White, (::TFTColorRegion *)colorRegions);
|
||||
#endif
|
||||
#if defined(MUZI_BASE)
|
||||
dispdev->delayPoweron = true;
|
||||
#endif
|
||||
#if defined(USE_ST7796) && defined(TFT_MESH)
|
||||
// Custom text color, if defined in variant.h
|
||||
static_cast<ST7796Spi *>(dispdev)->setRGB(TFT_MESH);
|
||||
#if defined(USE_ST7796)
|
||||
static_cast<ST7796Spi *>(dispdev)->setRGB(TFTPalette::White);
|
||||
#endif
|
||||
|
||||
// Initialize display and UI system
|
||||
@@ -620,7 +696,7 @@ void Screen::setup()
|
||||
static OverlayCallback overlays[] = {
|
||||
graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame
|
||||
};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setOverlays(overlays, 1);
|
||||
|
||||
// Enable UTF-8 to display mapping
|
||||
dispdev->setFontTableLookupFunction(customFontTableLookup);
|
||||
@@ -639,7 +715,7 @@ void Screen::setup()
|
||||
#endif
|
||||
{
|
||||
const char *region = myRegion ? myRegion->name : nullptr;
|
||||
graphics::UIRenderer::drawIconScreen(region, display, state, x, y);
|
||||
graphics::UIRenderer::drawBootIconScreen(region, display, state, x, y);
|
||||
}
|
||||
};
|
||||
ui->setFrames(alertFrames, 1);
|
||||
@@ -678,9 +754,9 @@ void Screen::setup()
|
||||
// Turn on display and trigger first draw
|
||||
handleSetOn(true);
|
||||
graphics::currentResolution = graphics::determineScreenResolution(dispdev->height(), dispdev->width());
|
||||
ui->update();
|
||||
updateUiFrame(ui);
|
||||
#ifndef USE_EINK
|
||||
ui->update(); // Some SSD1306 clones drop the first draw, so run twice
|
||||
updateUiFrame(ui); // Some SSD1306 clones drop the first draw, so run twice
|
||||
#endif
|
||||
serialSinceMsec = millis();
|
||||
|
||||
@@ -753,7 +829,7 @@ void Screen::forceDisplay(bool forceUiUpdate)
|
||||
do {
|
||||
startUpdate = millis(); // Handle impossibly unlikely corner case of a millis() overflow..
|
||||
delay(10);
|
||||
ui->update();
|
||||
updateUiFrame(ui);
|
||||
} while (ui->getUiState()->lastUpdate < startUpdate);
|
||||
|
||||
// Return to normal frame rate
|
||||
@@ -824,9 +900,9 @@ int32_t Screen::runOnce()
|
||||
static FrameCallback bootOEMFrames[] = {graphics::UIRenderer::drawOEMBootScreen};
|
||||
static const int bootOEMFrameCount = sizeof(bootOEMFrames) / sizeof(bootOEMFrames[0]);
|
||||
ui->setFrames(bootOEMFrames, bootOEMFrameCount);
|
||||
ui->update();
|
||||
updateUiFrame(ui);
|
||||
#ifndef USE_EINK
|
||||
ui->update();
|
||||
updateUiFrame(ui);
|
||||
#endif
|
||||
showingOEMBootScreen = false;
|
||||
}
|
||||
@@ -917,15 +993,28 @@ int32_t Screen::runOnce()
|
||||
|
||||
// this must be before the frameState == FIXED check, because we always
|
||||
// want to draw at least one FIXED frame before doing forceDisplay
|
||||
ui->update();
|
||||
updateUiFrame(ui);
|
||||
|
||||
// Switch to a low framerate (to save CPU) when we are not in transition
|
||||
// but we should only call setTargetFPS when framestate changes, because
|
||||
// otherwise that breaks animations.
|
||||
|
||||
if (targetFramerate != IDLE_FRAMERATE && ui->getUiState()->frameState == FIXED) {
|
||||
uint32_t desiredFramerate = IDLE_FRAMERATE;
|
||||
#if HAS_GPS && !defined(USE_EINK)
|
||||
if (showingNormalScreen && hasCompass) {
|
||||
const uint8_t currentFrame = ui->getUiState()->currentFrame;
|
||||
if ((framesetInfo.positions.gps != 255 && currentFrame == framesetInfo.positions.gps) ||
|
||||
(framesetInfo.positions.waypoint != 255 && currentFrame == framesetInfo.positions.waypoint) ||
|
||||
(framesetInfo.positions.firstFavorite != 255 && currentFrame >= framesetInfo.positions.firstFavorite &&
|
||||
currentFrame <= framesetInfo.positions.lastFavorite)) {
|
||||
desiredFramerate = COMPASS_ACTIVE_FRAMERATE;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (targetFramerate != desiredFramerate && ui->getUiState()->frameState == FIXED) {
|
||||
// oldFrameState = ui->getUiState()->frameState;
|
||||
targetFramerate = IDLE_FRAMERATE;
|
||||
targetFramerate = desiredFramerate;
|
||||
|
||||
ui->setTargetFPS(targetFramerate);
|
||||
forceDisplay();
|
||||
@@ -966,7 +1055,7 @@ void Screen::setSSLFrames()
|
||||
// LOG_DEBUG("Show SSL frames");
|
||||
static FrameCallback sslFrames[] = {NotificationRenderer::drawSSLScreen};
|
||||
ui->setFrames(sslFrames, 1);
|
||||
ui->update();
|
||||
updateUiFrame(ui);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1002,7 +1091,7 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver)
|
||||
do {
|
||||
startUpdate = millis(); // Handle impossibly unlikely corner case of a millis() overflow..
|
||||
delay(1);
|
||||
ui->update();
|
||||
updateUiFrame(ui);
|
||||
} while (ui->getUiState()->lastUpdate < startUpdate);
|
||||
|
||||
#if defined(USE_EINK_PARALLELDISPLAY)
|
||||
@@ -1196,8 +1285,8 @@ void Screen::setFrames(FrameFocus focus)
|
||||
|
||||
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||
const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
|
||||
if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) {
|
||||
favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo);
|
||||
if (n && n->num != nodeDB->getNodeNum() && nodeInfoLiteIsFavorite(n)) {
|
||||
favoriteFrames.push_back(graphics::UIRenderer::drawFavoriteNode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1224,9 +1313,9 @@ void Screen::setFrames(FrameFocus focus)
|
||||
|
||||
// Add overlays: frame icons and alert banner)
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setOverlays(overlays, 2);
|
||||
|
||||
prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list just changed)
|
||||
prevFrame = -1; // Force drawFavoriteNode to pick a new node (because our list just changed)
|
||||
|
||||
// Focus on a specific frame, in the frame set we just created
|
||||
switch (focus) {
|
||||
@@ -1377,9 +1466,15 @@ void Screen::blink()
|
||||
dispdev->setBrightness(254);
|
||||
while (count > 0) {
|
||||
dispdev->fillRect(0, 0, dispdev->getWidth(), dispdev->getHeight());
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
prepareFrameColorRegions();
|
||||
#endif
|
||||
dispdev->display();
|
||||
delay(50);
|
||||
dispdev->clear();
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
prepareFrameColorRegions();
|
||||
#endif
|
||||
dispdev->display();
|
||||
delay(50);
|
||||
count = count - 1;
|
||||
@@ -1513,6 +1608,9 @@ void Screen::setFastFramerate()
|
||||
{
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
dispdev->clear();
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
prepareFrameColorRegions();
|
||||
#endif
|
||||
dispdev->display();
|
||||
#endif
|
||||
// We are about to start a transition so speed up fps
|
||||
@@ -1545,138 +1643,6 @@ int Screen::handleStatusUpdate(const meshtastic::Status *arg)
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Handles when message is received; will jump to text message frame.
|
||||
int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
|
||||
{
|
||||
if (showingNormalScreen) {
|
||||
if (packet->from == 0) {
|
||||
// Outgoing message (likely sent from phone)
|
||||
devicestate.has_rx_text_message = false;
|
||||
memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message));
|
||||
hiddenFrames.textMessage = true;
|
||||
hasUnreadMessage = false; // Clear unread state when user replies
|
||||
|
||||
setFrames(FOCUS_PRESERVE); // Stay on same frame, silently update frame list
|
||||
} else {
|
||||
// Incoming message
|
||||
devicestate.has_rx_text_message = true; // Needed to include the message frame
|
||||
hasUnreadMessage = true; // Enables mail icon in the header
|
||||
setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view (no-op during text_input)
|
||||
|
||||
// Only wake/force display if the configuration allows it
|
||||
if (shouldWakeOnReceivedMessage()) {
|
||||
setOn(true); // Wake up the screen first
|
||||
forceDisplay(); // Forces screen redraw
|
||||
}
|
||||
// === Prepare banner/popup content ===
|
||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from);
|
||||
const meshtastic_Channel channel =
|
||||
channels.getByIndex(packet->channel ? packet->channel : channels.getPrimaryIndex());
|
||||
const char *longName = (node && node->has_user) ? node->user.long_name : nullptr;
|
||||
|
||||
const char *msgRaw = reinterpret_cast<const char *>(packet->decoded.payload.bytes);
|
||||
|
||||
char banner[256];
|
||||
|
||||
bool isAlert = false;
|
||||
|
||||
if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_bell_vibra ||
|
||||
moduleConfig.external_notification.alert_bell_buzzer)
|
||||
// Check for bell character to determine if this message is an alert
|
||||
for (size_t i = 0; i < packet->decoded.payload.size && i < 100; i++) {
|
||||
if (msgRaw[i] == ASCII_BELL) {
|
||||
isAlert = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Unlike generic messages, alerts (when enabled via the ext notif module) ignore any
|
||||
// 'mute' preferences set to any specific node or channel.
|
||||
// If on-screen keyboard is active, show a transient popup over keyboard instead of interrupting it
|
||||
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
|
||||
// Wake and force redraw so popup is visible immediately
|
||||
if (shouldWakeOnReceivedMessage()) {
|
||||
setOn(true);
|
||||
forceDisplay();
|
||||
}
|
||||
|
||||
// Build popup: title = message source name, content = message text (sanitized)
|
||||
// Title
|
||||
char titleBuf[64] = {0};
|
||||
if (longName && longName[0]) {
|
||||
// Sanitize sender name
|
||||
std::string t = sanitizeString(longName);
|
||||
strncpy(titleBuf, t.c_str(), sizeof(titleBuf) - 1);
|
||||
} else {
|
||||
strncpy(titleBuf, "Message", sizeof(titleBuf) - 1);
|
||||
}
|
||||
|
||||
// Content: payload bytes may not be null-terminated, remove ASCII_BELL and sanitize
|
||||
char content[256] = {0};
|
||||
{
|
||||
std::string raw;
|
||||
raw.reserve(packet->decoded.payload.size);
|
||||
for (size_t i = 0; i < packet->decoded.payload.size; ++i) {
|
||||
char c = msgRaw[i];
|
||||
if (c == ASCII_BELL)
|
||||
continue; // strip bell
|
||||
raw.push_back(c);
|
||||
}
|
||||
std::string sanitized = sanitizeString(raw);
|
||||
strncpy(content, sanitized.c_str(), sizeof(content) - 1);
|
||||
}
|
||||
|
||||
NotificationRenderer::showKeyboardMessagePopupWithTitle(titleBuf, content, 3000);
|
||||
|
||||
// Maintain existing buzzer behavior on M5 if applicable
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY ||
|
||||
(isAlert && moduleConfig.external_notification.alert_bell_buzzer) ||
|
||||
(!isBroadcast(packet->to) && isToUs(packet))) {
|
||||
playLongBeep();
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
// No keyboard active: use regular banner flow, respecting mute settings
|
||||
if (isAlert) {
|
||||
if (longName && longName[0]) {
|
||||
snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName);
|
||||
} else {
|
||||
strcpy(banner, "Alert Received");
|
||||
}
|
||||
screen->showSimpleBanner(banner, 3000);
|
||||
} else if (!channel.settings.has_module_settings || !channel.settings.module_settings.is_muted) {
|
||||
if (longName && longName[0]) {
|
||||
if (currentResolution == ScreenResolution::UltraLow) {
|
||||
strcpy(banner, "New Message");
|
||||
} else {
|
||||
snprintf(banner, sizeof(banner), "New Message from\n%s", longName);
|
||||
}
|
||||
} else {
|
||||
strcpy(banner, "New Message");
|
||||
}
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
screen->setOn(true);
|
||||
screen->showSimpleBanner(banner, 1500);
|
||||
if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY ||
|
||||
(isAlert && moduleConfig.external_notification.alert_bell_buzzer) ||
|
||||
(!isBroadcast(packet->to) && isToUs(packet))) {
|
||||
// Beep if not in DIRECT_MSG_ONLY mode or if in DIRECT_MSG_ONLY mode and either
|
||||
// - packet contains an alert and alert bell buzzer is enabled
|
||||
// - packet is a non-broadcast that is addressed to this node
|
||||
playLongBeep();
|
||||
}
|
||||
#else
|
||||
screen->showSimpleBanner(banner, 3000);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Triggered by MeshModules
|
||||
int Screen::handleUIFrameEvent(const UIFrameEvent *event)
|
||||
{
|
||||
@@ -1722,9 +1688,9 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
|
||||
NotificationRenderer::inEvent = *event;
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setOverlays(overlays, 2);
|
||||
setFastFramerate(); // Draw ASAP
|
||||
ui->update();
|
||||
updateUiFrame(ui);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1737,9 +1703,9 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
if (NotificationRenderer::isOverlayBannerShowing()) {
|
||||
NotificationRenderer::inEvent = *event;
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setOverlays(overlays, 2);
|
||||
setFastFramerate(); // Draw ASAP
|
||||
ui->update();
|
||||
updateUiFrame(ui);
|
||||
|
||||
menuHandler::handleMenuSwitch(dispdev);
|
||||
return 0;
|
||||
|
||||
@@ -330,15 +330,11 @@ class Screen : public concurrency::OSThread
|
||||
|
||||
// Function to allow the AccelerometerThread to set the heading if a sensor provides it
|
||||
// Mutex needed?
|
||||
void setHeading(long _heading)
|
||||
{
|
||||
hasCompass = true;
|
||||
compassHeading = fmod(_heading, 360);
|
||||
}
|
||||
void setHeading(float heading);
|
||||
|
||||
bool hasHeading() { return hasCompass; }
|
||||
|
||||
long getHeading() { return compassHeading; }
|
||||
float getHeading() { return compassHeading; }
|
||||
|
||||
void setEndCalibration(uint32_t _endCalibrationAt) { endCalibrationAt = _endCalibrationAt; }
|
||||
uint32_t getEndCalibration() { return endCalibrationAt; }
|
||||
@@ -613,7 +609,6 @@ class Screen : public concurrency::OSThread
|
||||
|
||||
// Handle observer events
|
||||
int handleStatusUpdate(const meshtastic::Status *arg);
|
||||
int handleTextMessage(const meshtastic_MeshPacket *packet);
|
||||
int handleUIFrameEvent(const UIFrameEvent *arg);
|
||||
int handleInputEvent(const InputEvent *arg);
|
||||
int handleAdminMessage(AdminModule_ObserverData *arg);
|
||||
@@ -792,4 +787,4 @@ extern std::vector<std::string> functionSymbol;
|
||||
extern std::string functionSymbolString;
|
||||
extern graphics::Screen *screen;
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -88,6 +88,16 @@
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// nRF52 flash optimization: re-route FONT_LARGE_LOCAL to the 16pt glyph so
|
||||
// the display-tier dispatch below picks up 16pt everywhere it would have used
|
||||
// 24pt. Drops the ~9.6 KB ArialMT_Plain_24 table from the linked binary.
|
||||
// Set MESHTASTIC_LARGE_FONT_24PT=1 in build_flags to opt out per variant
|
||||
// (undefined or 0 keeps the optimization on).
|
||||
#if defined(ARCH_NRF52) && (!defined(MESHTASTIC_LARGE_FONT_24PT) || MESHTASTIC_LARGE_FONT_24PT == 0)
|
||||
#undef FONT_LARGE_LOCAL
|
||||
#define FONT_LARGE_LOCAL FONT_MEDIUM_LOCAL
|
||||
#endif
|
||||
|
||||
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
||||
defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \
|
||||
defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR)) && \
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
#include "configuration.h"
|
||||
#if HAS_SCREEN
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "RTC.h"
|
||||
#include "draw/NodeListRenderer.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/TFTColorRegions.h"
|
||||
#include "graphics/TFTPalette.h"
|
||||
#include "graphics/draw/UIRenderer.h"
|
||||
#include "main.h"
|
||||
#include "meshtastic/config.pb.h"
|
||||
#include "modules/ExternalNotificationModule.h"
|
||||
#include "power.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#include <cctype>
|
||||
#include <graphics/images.h>
|
||||
|
||||
namespace graphics
|
||||
@@ -65,6 +69,12 @@ uint32_t lastBlinkShared = 0;
|
||||
bool isMailIconVisible = true;
|
||||
uint32_t lastMailBlink = 0;
|
||||
|
||||
static inline bool useClockHeaderAccentTheme(uint32_t themeId)
|
||||
{
|
||||
return themeId == ThemeID::Pink || themeId == ThemeID::Creamsicle || themeId == ThemeID::MeshtasticGreen ||
|
||||
themeId == ThemeID::ClassicRed || themeId == ThemeID::MonochromeWhite;
|
||||
}
|
||||
|
||||
// *********************************
|
||||
// * Rounded Header when inverted *
|
||||
// *********************************
|
||||
@@ -85,7 +95,8 @@ void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w,
|
||||
// *************************
|
||||
// * Common Header Drawing *
|
||||
// *************************
|
||||
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date)
|
||||
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date,
|
||||
bool transparent_background, bool use_title_color_override, uint16_t title_color_override)
|
||||
{
|
||||
constexpr int HEADER_OFFSET_Y = 1;
|
||||
y += HEADER_OFFSET_Y;
|
||||
@@ -100,30 +111,93 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
|
||||
const int screenW = display->getWidth();
|
||||
const int screenH = display->getHeight();
|
||||
const int headerHeight = highlightHeight + 2;
|
||||
const uint16_t headerColorForRoles = getThemeHeaderBg();
|
||||
// Color TFT headers use a fixed dark background + white glyphs.
|
||||
// Keep legacy inverted bitmap behavior only for monochrome displays.
|
||||
const bool useInvertedHeaderStyle = (isInverted && !force_no_invert && !isTFTColoringEnabled() && !transparent_background);
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
int statusLeftEndX = 0;
|
||||
int statusRightStartX = screenW;
|
||||
const bool isClockHeader = transparent_background && show_date && (!titleStr || titleStr[0] == '\0');
|
||||
const auto activeThemeId = getActiveTheme().id;
|
||||
const bool useClockHeaderAccent = isClockHeader && useClockHeaderAccentTheme(activeThemeId);
|
||||
#endif
|
||||
|
||||
{
|
||||
const uint16_t headerColor = getThemeHeaderBg();
|
||||
const uint16_t headerTextColor = getThemeHeaderText();
|
||||
const uint16_t headerTitleColorForRole = use_title_color_override ? title_color_override : headerTextColor;
|
||||
uint16_t headerStatusColor = getThemeHeaderStatus();
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
// Clock frame uses transparent header + date + empty title.
|
||||
// For accent clock themes (Pink/Creamsicle + classic monochrome), tint
|
||||
// status items (battery outline, %, date, mail icon) to the header accent.
|
||||
if (useClockHeaderAccent) {
|
||||
headerStatusColor = getThemeHeaderBg();
|
||||
}
|
||||
|
||||
if (transparent_background) {
|
||||
// Transparent clock headers should inherit whatever body off-color is
|
||||
// already active under the header (important for light/inverted themes).
|
||||
const uint16_t transparentBgColor = resolveTFTOffColorAt(0, headerHeight + 1, getThemeBodyBg());
|
||||
setAndRegisterTFTColorRole(TFTColorRole::HeaderBackground, transparentBgColor, transparentBgColor, 0, 0, screenW,
|
||||
headerHeight);
|
||||
setTFTColorRole(TFTColorRole::HeaderTitle, headerTitleColorForRole, transparentBgColor);
|
||||
setTFTColorRole(TFTColorRole::HeaderStatus, headerStatusColor, transparentBgColor);
|
||||
} else if (useInvertedHeaderStyle) {
|
||||
setAndRegisterTFTColorRole(TFTColorRole::HeaderBackground, headerColor, TFTPalette::Black, 0, 0, screenW,
|
||||
headerHeight);
|
||||
setTFTColorRole(TFTColorRole::HeaderTitle, headerColor, headerTitleColorForRole);
|
||||
setTFTColorRole(TFTColorRole::HeaderStatus, headerColor, headerStatusColor);
|
||||
} else {
|
||||
setAndRegisterTFTColorRole(TFTColorRole::HeaderBackground, TFTPalette::Black, headerColor, 0, 0, screenW,
|
||||
headerHeight);
|
||||
setTFTColorRole(TFTColorRole::HeaderTitle, headerTitleColorForRole, headerColor);
|
||||
setTFTColorRole(TFTColorRole::HeaderStatus, headerStatusColor, headerColor);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!force_no_invert) {
|
||||
// === Inverted Header Background ===
|
||||
if (isInverted) {
|
||||
if (useInvertedHeaderStyle) {
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(0, 0, screenW, highlightHeight + 2);
|
||||
display->fillRect(0, 0, screenW, headerHeight);
|
||||
display->setColor(WHITE);
|
||||
drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2);
|
||||
display->setColor(BLACK);
|
||||
} else {
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(0, 0, screenW, highlightHeight + 2);
|
||||
display->setColor(WHITE);
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
display->drawLine(0, 20, screenW, 20);
|
||||
} else {
|
||||
display->drawLine(0, 14, screenW, 14);
|
||||
display->fillRect(0, 0, screenW, headerHeight);
|
||||
// Keep the legacy white separator for monochrome displays only when header background is visible.
|
||||
#if !GRAPHICS_TFT_COLORING_ENABLED
|
||||
if (!transparent_background) {
|
||||
display->setColor(WHITE);
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
display->drawLine(0, 20, screenW, 20);
|
||||
} else {
|
||||
display->drawLine(0, 14, screenW, 14);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
if (transparent_background) {
|
||||
display->setColor(WHITE);
|
||||
}
|
||||
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
// TFT role coloring expects foreground glyph bits to be "set".
|
||||
display->setColor(WHITE);
|
||||
#endif
|
||||
|
||||
// === Screen Title ===
|
||||
const char *headerTitle = titleStr ? titleStr : "";
|
||||
const int titleWidth = UIRenderer::measureStringWithEmotes(display, headerTitle);
|
||||
const int titleX = (SCREEN_WIDTH - titleWidth) / 2;
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
const int titleRegionWidth = titleWidth + (config.display.heading_bold ? 3 : 2);
|
||||
registerTFTColorRegion(TFTColorRole::HeaderTitle, titleX - 1, y, titleRegionWidth, FONT_HEIGHT_SMALL);
|
||||
#endif
|
||||
UIRenderer::drawStringWithEmotes(display, titleX, y, headerTitle, FONT_HEIGHT_SMALL, 1, config.display.heading_bold);
|
||||
}
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
@@ -152,6 +226,17 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
|
||||
bool useHorizontalBattery = (currentResolution == ScreenResolution::High && screenW >= screenH);
|
||||
const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2;
|
||||
bool hasBatteryFillRegion = false;
|
||||
int16_t batteryFillRegionX = 0;
|
||||
int16_t batteryFillRegionY = 0;
|
||||
int16_t batteryFillRegionW = 0;
|
||||
int16_t batteryFillRegionH = 0;
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
uint16_t batteryFillColor = getThemeBatteryFillColor(chargePercent);
|
||||
if (useClockHeaderAccent) {
|
||||
batteryFillColor = getThemeHeaderBg();
|
||||
}
|
||||
#endif
|
||||
|
||||
int batteryX = 1;
|
||||
int batteryY = HEADER_OFFSET_Y + 1;
|
||||
@@ -180,6 +265,15 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
display->drawLine(batteryX + 5, batteryY + 12, batteryX + 10, batteryY + 12);
|
||||
int fillWidth = 14 * chargePercent / 100;
|
||||
display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 11);
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
if (fillWidth > 0) {
|
||||
hasBatteryFillRegion = true;
|
||||
batteryFillRegionX = batteryX + 1;
|
||||
batteryFillRegionY = batteryY + 1;
|
||||
batteryFillRegionW = fillWidth;
|
||||
batteryFillRegionH = 11;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
batteryX += 18; // Icon + 2 pixels
|
||||
} else {
|
||||
@@ -194,21 +288,41 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
int fillHeight = 8 * chargePercent / 100;
|
||||
int fillY = batteryY - fillHeight;
|
||||
display->fillRect(batteryX + 1, fillY + 10, 5, fillHeight);
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
if (fillHeight > 0) {
|
||||
hasBatteryFillRegion = true;
|
||||
batteryFillRegionX = batteryX + 1;
|
||||
batteryFillRegionY = fillY + 10;
|
||||
batteryFillRegionW = 5;
|
||||
batteryFillRegionH = fillHeight;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
batteryX += 9; // Icon + 2 pixels
|
||||
}
|
||||
}
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
statusLeftEndX = batteryX + 2;
|
||||
#endif
|
||||
|
||||
if (chargePercent != 101) {
|
||||
// === Battery % Display ===
|
||||
char chargeStr[4];
|
||||
snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent);
|
||||
int chargeNumWidth = display->getStringWidth(chargeStr);
|
||||
const int percentWidth = display->getStringWidth("%");
|
||||
const int percentX = batteryX + chargeNumWidth - 1;
|
||||
display->drawString(batteryX, textY, chargeStr);
|
||||
display->drawString(batteryX + chargeNumWidth - 1, textY, "%");
|
||||
display->drawString(percentX, textY, "%");
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
statusLeftEndX = percentX + percentWidth + 2;
|
||||
#endif
|
||||
if (isBold) {
|
||||
display->drawString(batteryX + 1, textY, chargeStr);
|
||||
display->drawString(batteryX + chargeNumWidth, textY, "%");
|
||||
display->drawString(percentX + 1, textY, "%");
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
statusLeftEndX = percentX + percentWidth + 3;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,6 +367,9 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
timeStrWidth = display->getStringWidth(timeStr);
|
||||
}
|
||||
timeX = screenW - xOffset - timeStrWidth + 3;
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
statusRightStartX = timeX - (useHorizontalBattery ? 22 : 16);
|
||||
#endif
|
||||
|
||||
// === Show Mail or Mute Icon to the Left of Time ===
|
||||
int iconRightEdge = timeX - 2;
|
||||
@@ -278,7 +395,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
int iconW = 16, iconH = 12;
|
||||
int iconX = iconRightEdge - iconW;
|
||||
int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1;
|
||||
if (isInverted && !force_no_invert) {
|
||||
if (useInvertedHeaderStyle) {
|
||||
display->setColor(WHITE);
|
||||
display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2);
|
||||
display->setColor(BLACK);
|
||||
@@ -293,7 +410,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
} else {
|
||||
int iconX = iconRightEdge - (mail_width - 2);
|
||||
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
||||
if (isInverted && !force_no_invert) {
|
||||
if (useInvertedHeaderStyle) {
|
||||
display->setColor(WHITE);
|
||||
display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2);
|
||||
display->setColor(BLACK);
|
||||
@@ -309,7 +426,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
int iconX = iconRightEdge - mute_symbol_big_width;
|
||||
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
|
||||
|
||||
if (isInverted && !force_no_invert) {
|
||||
if (useInvertedHeaderStyle) {
|
||||
display->setColor(WHITE);
|
||||
display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2);
|
||||
display->setColor(BLACK);
|
||||
@@ -323,7 +440,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
int iconX = iconRightEdge - mute_symbol_width;
|
||||
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
||||
|
||||
if (isInverted && !force_no_invert) {
|
||||
if (useInvertedHeaderStyle) {
|
||||
display->setColor(WHITE);
|
||||
display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2);
|
||||
display->setColor(BLACK);
|
||||
@@ -351,7 +468,9 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
} else {
|
||||
// === No Time Available: Mail/Mute Icon Moves to Far Right ===
|
||||
int iconRightEdge = screenW - xOffset;
|
||||
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
statusRightStartX = screenW - (useHorizontalBattery ? 22 : 12);
|
||||
#endif
|
||||
bool showMail = false;
|
||||
|
||||
#ifndef USE_EINK
|
||||
@@ -393,6 +512,16 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
registerTFTColorRegion(TFTColorRole::HeaderStatus, 0, 0, statusLeftEndX, headerHeight);
|
||||
if (statusRightStartX < screenW) {
|
||||
registerTFTColorRegion(TFTColorRole::HeaderStatus, statusRightStartX, 0, screenW - statusRightStartX, headerHeight);
|
||||
}
|
||||
if (hasBatteryFillRegion) {
|
||||
registerTFTColorRegionDirect(batteryFillRegionX, batteryFillRegionY, batteryFillRegionW, batteryFillRegionH,
|
||||
batteryFillColor, headerColorForRoles);
|
||||
}
|
||||
#endif
|
||||
display->setColor(WHITE); // Reset for other UI
|
||||
}
|
||||
@@ -430,14 +559,23 @@ void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y)
|
||||
return;
|
||||
|
||||
const int scale = (currentResolution == ScreenResolution::High) ? 2 : 1;
|
||||
const int footerY = SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale);
|
||||
const int footerH = (connection_icon_height * scale) + (2 * scale);
|
||||
const int iconX = 0;
|
||||
const int iconY = SCREEN_HEIGHT - (connection_icon_height * scale);
|
||||
const int iconW = connection_icon_width * scale;
|
||||
const int iconH = connection_icon_height * scale;
|
||||
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
// Only tint the link glyph itself on TFT; keep the footer background black.
|
||||
setAndRegisterTFTColorRole(TFTColorRole::ConnectionIcon, TFTPalette::Blue, TFTPalette::Black, iconX, iconY, iconW, iconH);
|
||||
#endif
|
||||
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale),
|
||||
(connection_icon_height * scale) + (2 * scale));
|
||||
display->fillRect(0, footerY, SCREEN_WIDTH, footerH);
|
||||
display->setColor(WHITE);
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
const int bytesPerRow = (connection_icon_width + 7) / 8;
|
||||
int iconX = 0;
|
||||
int iconY = SCREEN_HEIGHT - (connection_icon_height * 2);
|
||||
|
||||
for (int yy = 0; yy < connection_icon_height; ++yy) {
|
||||
const uint8_t *rowPtr = connection_icon + yy * bytesPerRow;
|
||||
@@ -451,65 +589,127 @@ void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y)
|
||||
}
|
||||
|
||||
} else {
|
||||
display->drawXbm(0, SCREEN_HEIGHT - connection_icon_height, connection_icon_width, connection_icon_height,
|
||||
connection_icon);
|
||||
display->drawXbm(iconX, iconY, connection_icon_width, connection_icon_height, connection_icon);
|
||||
}
|
||||
}
|
||||
|
||||
bool isAllowedPunctuation(char c)
|
||||
{
|
||||
const std::string allowed = ".,!?;:-_()[]{}'\"@#$/\\&+=%~^ ";
|
||||
return allowed.find(c) != std::string::npos;
|
||||
switch (c) {
|
||||
case '.':
|
||||
case ',':
|
||||
case '!':
|
||||
case '?':
|
||||
case ';':
|
||||
case ':':
|
||||
case '-':
|
||||
case '_':
|
||||
case '(':
|
||||
case ')':
|
||||
case '[':
|
||||
case ']':
|
||||
case '{':
|
||||
case '}':
|
||||
case '\'':
|
||||
case '"':
|
||||
case '@':
|
||||
case '#':
|
||||
case '$':
|
||||
case '/':
|
||||
case '\\':
|
||||
case '&':
|
||||
case '+':
|
||||
case '=':
|
||||
case '%':
|
||||
case '~':
|
||||
case '^':
|
||||
case ' ':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static void replaceAll(std::string &s, const std::string &from, const std::string &to)
|
||||
static inline size_t utf8CodePointLength(unsigned char lead)
|
||||
{
|
||||
if (from.empty())
|
||||
return;
|
||||
size_t pos = 0;
|
||||
while ((pos = s.find(from, pos)) != std::string::npos) {
|
||||
s.replace(pos, from.size(), to);
|
||||
pos += to.size();
|
||||
if ((lead & 0x80) == 0x00) {
|
||||
return 1;
|
||||
}
|
||||
if ((lead & 0xE0) == 0xC0) {
|
||||
return 2;
|
||||
}
|
||||
if ((lead & 0xF0) == 0xE0) {
|
||||
return 3;
|
||||
}
|
||||
if ((lead & 0xF8) == 0xF0) {
|
||||
return 4;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string sanitizeString(const std::string &input)
|
||||
{
|
||||
static constexpr char kReplacementChar = static_cast<char>(0xBF); // Inverted question mark in ISO-8859-1.
|
||||
std::string output;
|
||||
output.reserve(input.size());
|
||||
bool inReplacement = false;
|
||||
|
||||
// Make a mutable copy so we can normalize UTF-8 “smart punctuation” into ASCII first.
|
||||
std::string s = input;
|
||||
|
||||
// Curly single quotes: ‘ ’
|
||||
replaceAll(s, "\xE2\x80\x98", "'"); // U+2018
|
||||
replaceAll(s, "\xE2\x80\x99", "'"); // U+2019
|
||||
|
||||
// Curly double quotes: “ ”
|
||||
replaceAll(s, "\xE2\x80\x9C", "\""); // U+201C
|
||||
replaceAll(s, "\xE2\x80\x9D", "\""); // U+201D
|
||||
|
||||
// En dash / Em dash: – —
|
||||
replaceAll(s, "\xE2\x80\x93", "-"); // U+2013
|
||||
replaceAll(s, "\xE2\x80\x94", "-"); // U+2014
|
||||
|
||||
// Non-breaking space
|
||||
replaceAll(s, "\xC2\xA0", " "); // U+00A0
|
||||
|
||||
// Now do your original sanitize pass over the normalized string.
|
||||
for (unsigned char uc : s) {
|
||||
char c = static_cast<char>(uc);
|
||||
if (std::isalnum(uc) || isAllowedPunctuation(c)) {
|
||||
output += c;
|
||||
inReplacement = false;
|
||||
} else {
|
||||
const size_t inputSize = input.size();
|
||||
size_t i = 0;
|
||||
while (i < inputSize) {
|
||||
const unsigned char byte0 = static_cast<unsigned char>(input[i]);
|
||||
char normalized = '\0';
|
||||
size_t consumed = 0;
|
||||
if (byte0 < 0x80) {
|
||||
normalized = static_cast<char>(byte0);
|
||||
consumed = 1;
|
||||
} else if ((i + 2) < inputSize && byte0 == 0xE2 && static_cast<unsigned char>(input[i + 1]) == 0x80) {
|
||||
// Smart punctuation: ' ' \" \" - -
|
||||
switch (static_cast<unsigned char>(input[i + 2])) {
|
||||
case 0x98:
|
||||
case 0x99:
|
||||
normalized = '\'';
|
||||
consumed = 3;
|
||||
break;
|
||||
case 0x9C:
|
||||
case 0x9D:
|
||||
normalized = '\"';
|
||||
consumed = 3;
|
||||
break;
|
||||
case 0x93:
|
||||
case 0x94:
|
||||
normalized = '-';
|
||||
consumed = 3;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if ((i + 1) < inputSize && byte0 == 0xC2 && static_cast<unsigned char>(input[i + 1]) == 0xA0) {
|
||||
// Non-breaking space.
|
||||
normalized = ' ';
|
||||
consumed = 2;
|
||||
}
|
||||
if (consumed == 0) {
|
||||
size_t seqLen = utf8CodePointLength(byte0);
|
||||
if (seqLen > (inputSize - i)) {
|
||||
seqLen = 1;
|
||||
}
|
||||
if (!inReplacement) {
|
||||
output += static_cast<char>(0xBF); // ISO-8859-1 for inverted question mark
|
||||
output.push_back(kReplacementChar);
|
||||
inReplacement = true;
|
||||
}
|
||||
i += seqLen;
|
||||
continue;
|
||||
}
|
||||
const unsigned char normalizedUc = static_cast<unsigned char>(normalized);
|
||||
if (std::isalnum(normalizedUc) || isAllowedPunctuation(normalized)) {
|
||||
output.push_back(normalized);
|
||||
inReplacement = false;
|
||||
} else if (!inReplacement) {
|
||||
output.push_back(kReplacementChar);
|
||||
inReplacement = true;
|
||||
}
|
||||
i += consumed;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <OLEDDisplay.h>
|
||||
#include <stdint.h>
|
||||
#include <string>
|
||||
|
||||
namespace graphics
|
||||
@@ -52,7 +53,8 @@ void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w,
|
||||
|
||||
// Shared battery/time/mail header
|
||||
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool force_no_invert = false,
|
||||
bool show_date = false);
|
||||
bool show_date = false, bool transparent_background = false, bool use_title_color_override = false,
|
||||
uint16_t title_color_override = 0);
|
||||
|
||||
// Shared battery/time/mail header
|
||||
void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y);
|
||||
|
||||
@@ -0,0 +1,819 @@
|
||||
#include "TFTColorRegions.h"
|
||||
#include "NodeDB.h"
|
||||
#include "TFTPalette.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
TFTColorRegion colorRegions[MAX_TFT_COLOR_REGIONS];
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
struct TFTRoleColorsBe {
|
||||
uint16_t onColorBe;
|
||||
uint16_t offColorBe;
|
||||
};
|
||||
|
||||
static uint8_t colorRegionCount = 0;
|
||||
static constexpr uint32_t kFnv1aOffsetBasis = 2166136261u;
|
||||
static constexpr uint32_t kFnv1aPrime = 16777619u;
|
||||
|
||||
static constexpr uint16_t toBe565(uint16_t color)
|
||||
{
|
||||
return static_cast<uint16_t>((color >> 8) | (color << 8));
|
||||
}
|
||||
|
||||
static constexpr bool kRoleIsBody[static_cast<size_t>(TFTColorRole::Count)] = {
|
||||
false, // HeaderBackground
|
||||
false, // HeaderTitle
|
||||
false, // HeaderStatus
|
||||
true, // SignalBars
|
||||
true, // ConnectionIcon
|
||||
true, // UtilizationFill
|
||||
true, // FavoriteNode
|
||||
true, // ActionMenuBorder
|
||||
true, // ActionMenuBody
|
||||
true, // ActionMenuTitle
|
||||
true, // FrameMono
|
||||
false, // BootSplash
|
||||
true, // FavoriteNodeBGHighlight
|
||||
false, // NavigationBar
|
||||
false // NavigationArrow
|
||||
};
|
||||
|
||||
static inline bool isBodyColorRole(TFTColorRole role)
|
||||
{
|
||||
return kRoleIsBody[static_cast<size_t>(role)];
|
||||
}
|
||||
|
||||
static inline bool isMonochromeTheme(uint32_t themeId)
|
||||
{
|
||||
return themeId == ThemeID::MeshtasticGreen || themeId == ThemeID::ClassicRed || themeId == ThemeID::MonochromeWhite;
|
||||
}
|
||||
|
||||
static inline uint16_t getMonochromeAccent(uint32_t themeId)
|
||||
{
|
||||
return (themeId == ThemeID::MeshtasticGreen) ? TFTPalette::MeshtasticGreen
|
||||
: (themeId == ThemeID::ClassicRed) ? TFTPalette::ClassicRed
|
||||
: TFTPalette::White;
|
||||
}
|
||||
|
||||
static inline void replaceColor(uint16_t &value, uint16_t from, uint16_t to)
|
||||
{
|
||||
if (value == from) {
|
||||
value = to;
|
||||
}
|
||||
}
|
||||
|
||||
static inline uint32_t fnv1aAppendByte(uint32_t hash, uint8_t value)
|
||||
{
|
||||
return (hash ^ value) * kFnv1aPrime;
|
||||
}
|
||||
|
||||
static inline uint32_t fnv1aAppendU16(uint32_t hash, uint16_t value)
|
||||
{
|
||||
hash = fnv1aAppendByte(hash, static_cast<uint8_t>(value & 0xFF));
|
||||
hash = fnv1aAppendByte(hash, static_cast<uint8_t>((value >> 8) & 0xFF));
|
||||
return hash;
|
||||
}
|
||||
|
||||
// Compile-time header color overrides (backward-compatible)
|
||||
#ifdef TFT_HEADER_BG_COLOR_OVERRIDE
|
||||
static constexpr uint16_t kHeaderBackground = TFT_HEADER_BG_COLOR_OVERRIDE;
|
||||
#else
|
||||
static constexpr uint16_t kHeaderBackground = TFTPalette::DarkGray;
|
||||
#endif
|
||||
|
||||
#ifdef TFT_HEADER_TITLE_COLOR_OVERRIDE
|
||||
static constexpr uint16_t kTitleColor = TFT_HEADER_TITLE_COLOR_OVERRIDE;
|
||||
#else
|
||||
static constexpr uint16_t kTitleColor = TFTPalette::White;
|
||||
#endif
|
||||
|
||||
#ifdef TFT_HEADER_STATUS_COLOR_OVERRIDE
|
||||
static constexpr uint16_t kStatusColor = TFT_HEADER_STATUS_COLOR_OVERRIDE;
|
||||
#else
|
||||
static constexpr uint16_t kStatusColor = TFTPalette::White;
|
||||
#endif
|
||||
|
||||
// Theme definitions
|
||||
// Stored in kThemes[] and looked up by matching uiconfig.screen_rgb_color
|
||||
// against each entry's .uniqueIdentifier field.
|
||||
|
||||
static const TFTThemeDef kThemes[] = {
|
||||
|
||||
// Default Dark (ThemeID::DefaultDark = 0)
|
||||
{
|
||||
ThemeID::DefaultDark, // id
|
||||
"Default Dark", // name
|
||||
0, // uniqueIdentifier
|
||||
// roles[TFTColorRole::Count]
|
||||
{
|
||||
{kHeaderBackground, TFTPalette::Black}, // HeaderBackground
|
||||
{kHeaderBackground, kTitleColor}, // HeaderTitle
|
||||
{kHeaderBackground, kStatusColor}, // HeaderStatus
|
||||
{TFTPalette::Good, TFTPalette::Black}, // SignalBars
|
||||
{TFTPalette::Blue, TFTPalette::Black}, // ConnectionIcon
|
||||
{TFTPalette::Good, TFTPalette::Black}, // UtilizationFill
|
||||
{TFTPalette::Yellow, TFTPalette::Black}, // FavoriteNode
|
||||
{TFTPalette::DarkGray, TFTPalette::Black}, // ActionMenuBorder
|
||||
{TFTPalette::White, TFTPalette::Black}, // ActionMenuBody
|
||||
{TFTPalette::DarkGray, TFTPalette::White}, // ActionMenuTitle
|
||||
{TFTPalette::Black, TFTPalette::White}, // FrameMono
|
||||
{TFTPalette::White, TFTPalette::Black}, // BootSplash
|
||||
{TFTPalette::Yellow, TFTPalette::Black}, // FavoriteNodeBGHighlight
|
||||
{kStatusColor, kHeaderBackground}, // NavigationBar (icon fg, bar bg)
|
||||
{kTitleColor, TFTPalette::Black}, // NavigationArrow (arrow fg, body bg)
|
||||
},
|
||||
TFTPalette::Good, // batteryFillGood
|
||||
TFTPalette::Medium, // batteryFillMedium
|
||||
TFTPalette::Bad, // batteryFillBad
|
||||
false, // fullFrameInvert
|
||||
true, // visible
|
||||
},
|
||||
|
||||
// Default Light (ThemeID::DefaultLight = 1)
|
||||
{
|
||||
ThemeID::DefaultLight, // id
|
||||
"Default Light", // name
|
||||
1, // uniqueIdentifier
|
||||
{
|
||||
{TFTPalette::LightGray, TFTPalette::Black}, // HeaderBackground
|
||||
{TFTPalette::LightGray, TFTPalette::Black}, // HeaderTitle
|
||||
{TFTPalette::LightGray, TFTPalette::Black}, // HeaderStatus
|
||||
{TFTPalette::Good, TFTPalette::White}, // SignalBars
|
||||
{TFTPalette::Blue, TFTPalette::White}, // ConnectionIcon
|
||||
{TFTPalette::Good, TFTPalette::White}, // UtilizationFill
|
||||
{TFTPalette::Black, TFTPalette::Yellow}, // FavoriteNode
|
||||
{TFTPalette::DarkGray, TFTPalette::White}, // ActionMenuBorder
|
||||
{TFTPalette::Black, TFTPalette::White}, // ActionMenuBody
|
||||
{TFTPalette::DarkGray, TFTPalette::Black}, // ActionMenuTitle
|
||||
{TFTPalette::Black, TFTPalette::White}, // FrameMono
|
||||
{TFTPalette::White, TFTPalette::Black}, // BootSplash
|
||||
{TFTPalette::Black, TFTPalette::Yellow}, // FavoriteNodeBGHighlight
|
||||
{TFTPalette::Black, TFTPalette::LightGray}, // NavigationBar (icon fg, bar bg)
|
||||
{TFTPalette::Black, TFTPalette::White}, // NavigationArrow (arrow fg, body bg)
|
||||
},
|
||||
TFTPalette::Good, // batteryFillGood
|
||||
TFTPalette::Medium, // batteryFillMedium
|
||||
TFTPalette::Bad, // batteryFillBad
|
||||
true, // fullFrameInvert
|
||||
true, // visible
|
||||
},
|
||||
|
||||
// Christmas (ThemeID::Christmas = 2)
|
||||
{
|
||||
ThemeID::Christmas, // id
|
||||
"Christmas", // name
|
||||
2, // uniqueIdentifier
|
||||
{
|
||||
{TFTPalette::ChristmasRed, TFTPalette::Black}, // HeaderBackground
|
||||
{TFTPalette::ChristmasRed, TFTPalette::Gold}, // HeaderTitle
|
||||
{TFTPalette::ChristmasRed, TFTPalette::Gold}, // HeaderStatus
|
||||
{TFTPalette::ChristmasGreen, TFTPalette::Pine}, // SignalBars
|
||||
{TFTPalette::Gold, TFTPalette::Pine}, // ConnectionIcon
|
||||
{TFTPalette::ChristmasGreen, TFTPalette::Pine}, // UtilizationFill
|
||||
{TFTPalette::Gold, TFTPalette::Pine}, // FavoriteNode
|
||||
{TFTPalette::ChristmasRed, TFTPalette::Pine}, // ActionMenuBorder
|
||||
{TFTPalette::White, TFTPalette::Pine}, // ActionMenuBody
|
||||
{TFTPalette::ChristmasRed, TFTPalette::White}, // ActionMenuTitle
|
||||
{TFTPalette::Pine, TFTPalette::White}, // FrameMono
|
||||
{TFTPalette::White, TFTPalette::ChristmasRed}, // BootSplash
|
||||
{TFTPalette::Gold, TFTPalette::Pine}, // FavoriteNodeBGHighlight
|
||||
{TFTPalette::Gold, TFTPalette::ChristmasRed}, // NavigationBar (icon fg, bar bg)
|
||||
{TFTPalette::Gold, TFTPalette::Pine}, // NavigationArrow (arrow fg, body bg)
|
||||
},
|
||||
TFTPalette::ChristmasGreen, // batteryFillGood
|
||||
TFTPalette::Gold, // batteryFillMedium
|
||||
TFTPalette::ChristmasRed, // batteryFillBad
|
||||
true, // fullFrameInvert
|
||||
false, // visible
|
||||
},
|
||||
|
||||
// Pink (ThemeID::Pink = 3) light variant
|
||||
{
|
||||
ThemeID::Pink, // id
|
||||
"Pink", // name
|
||||
3, // uniqueIdentifier
|
||||
{
|
||||
{TFTPalette::HotPink, TFTPalette::Black}, // HeaderBackground
|
||||
{TFTPalette::HotPink, TFTPalette::White}, // HeaderTitle
|
||||
{TFTPalette::HotPink, TFTPalette::White}, // HeaderStatus
|
||||
{TFTPalette::DeepPink, TFTPalette::PalePink}, // SignalBars
|
||||
{TFTPalette::HotPink, TFTPalette::PalePink}, // ConnectionIcon
|
||||
{TFTPalette::DeepPink, TFTPalette::PalePink}, // UtilizationFill
|
||||
{TFTPalette::Black, TFTPalette::HotPink}, // FavoriteNode
|
||||
{TFTPalette::HotPink, TFTPalette::PalePink}, // ActionMenuBorder
|
||||
{TFTPalette::Black, TFTPalette::PalePink}, // ActionMenuBody
|
||||
{TFTPalette::HotPink, TFTPalette::White}, // ActionMenuTitle
|
||||
{TFTPalette::Black, TFTPalette::White}, // FrameMono
|
||||
{TFTPalette::White, TFTPalette::HotPink}, // BootSplash
|
||||
{TFTPalette::Black, TFTPalette::HotPink}, // FavoriteNodeBGHighlight
|
||||
{TFTPalette::White, TFTPalette::HotPink}, // NavigationBar (icon fg, bar bg)
|
||||
{TFTPalette::HotPink, TFTPalette::PalePink}, // NavigationArrow (arrow fg, body bg)
|
||||
},
|
||||
TFTPalette::DeepPink, // batteryFillGood
|
||||
TFTPalette::HotPink, // batteryFillMedium
|
||||
TFTPalette::Bad, // batteryFillBad
|
||||
true, // fullFrameInvert
|
||||
true, // visible
|
||||
},
|
||||
|
||||
// Blue (ThemeID::Blue = 4) dark variant
|
||||
{
|
||||
ThemeID::Blue, // id
|
||||
"Blue", // name
|
||||
4, // uniqueIdentifier
|
||||
{
|
||||
{TFTPalette::DeepBlue, TFTPalette::Black}, // HeaderBackground
|
||||
{TFTPalette::DeepBlue, TFTPalette::White}, // HeaderTitle
|
||||
{TFTPalette::DeepBlue, TFTPalette::SkyBlue}, // HeaderStatus
|
||||
{TFTPalette::SkyBlue, TFTPalette::Navy}, // SignalBars
|
||||
{TFTPalette::SkyBlue, TFTPalette::Navy}, // ConnectionIcon
|
||||
{TFTPalette::SkyBlue, TFTPalette::Navy}, // UtilizationFill
|
||||
{TFTPalette::SkyBlue, TFTPalette::Navy}, // FavoriteNode
|
||||
{TFTPalette::DeepBlue, TFTPalette::Navy}, // ActionMenuBorder
|
||||
{TFTPalette::White, TFTPalette::Navy}, // ActionMenuBody
|
||||
{TFTPalette::DeepBlue, TFTPalette::White}, // ActionMenuTitle
|
||||
{TFTPalette::Navy, TFTPalette::White}, // FrameMono
|
||||
{TFTPalette::White, TFTPalette::DeepBlue}, // BootSplash
|
||||
{TFTPalette::SkyBlue, TFTPalette::Navy}, // FavoriteNodeBGHighlight
|
||||
{TFTPalette::SkyBlue, TFTPalette::DeepBlue}, // NavigationBar (icon fg, bar bg)
|
||||
{TFTPalette::SkyBlue, TFTPalette::Black}, // NavigationArrow (arrow fg, body bg)
|
||||
},
|
||||
TFTPalette::SkyBlue, // batteryFillGood
|
||||
TFTPalette::Medium, // batteryFillMedium
|
||||
TFTPalette::Bad, // batteryFillBad
|
||||
true, // fullFrameInvert
|
||||
true, // visible
|
||||
},
|
||||
|
||||
// Creamsicle (ThemeID::Creamsicle = 5)light variant
|
||||
{
|
||||
ThemeID::Creamsicle, // id
|
||||
"Creamsicle", // name
|
||||
5, // uniqueIdentifier
|
||||
{
|
||||
{TFTPalette::CreamOrange, TFTPalette::Black}, // HeaderBackground
|
||||
{TFTPalette::CreamOrange, TFTPalette::White}, // HeaderTitle
|
||||
{TFTPalette::CreamOrange, TFTPalette::White}, // HeaderStatus
|
||||
{TFTPalette::DeepOrange, TFTPalette::Cream}, // SignalBars
|
||||
{TFTPalette::CreamOrange, TFTPalette::Cream}, // ConnectionIcon
|
||||
{TFTPalette::DeepOrange, TFTPalette::Cream}, // UtilizationFill
|
||||
{TFTPalette::Black, TFTPalette::CreamOrange}, // FavoriteNode
|
||||
{TFTPalette::CreamOrange, TFTPalette::Cream}, // ActionMenuBorder
|
||||
{TFTPalette::Black, TFTPalette::Cream}, // ActionMenuBody
|
||||
{TFTPalette::CreamOrange, TFTPalette::White}, // ActionMenuTitle
|
||||
{TFTPalette::Black, TFTPalette::White}, // FrameMono
|
||||
{TFTPalette::White, TFTPalette::CreamOrange}, // BootSplash
|
||||
{TFTPalette::Black, TFTPalette::CreamOrange}, // FavoriteNodeBGHighlight
|
||||
{TFTPalette::White, TFTPalette::CreamOrange}, // NavigationBar (icon fg, bar bg)
|
||||
{TFTPalette::CreamOrange, TFTPalette::White}, // NavigationArrow (arrow fg, body bg)
|
||||
},
|
||||
TFTPalette::DeepOrange, // batteryFillGood
|
||||
TFTPalette::Gold, // batteryFillMedium
|
||||
TFTPalette::Bad, // batteryFillBad
|
||||
true, // fullFrameInvert
|
||||
true, // visible
|
||||
},
|
||||
|
||||
// Meshtastic Green (ThemeID::MeshtasticGreen = 6) classic monochrome
|
||||
// Pure single-color-on-black look. Every role maps foreground pixels to
|
||||
// the theme color and background pixels to Black.
|
||||
{
|
||||
ThemeID::MeshtasticGreen, // id
|
||||
"Meshtastic Green", // name
|
||||
6, // uniqueIdentifier
|
||||
{
|
||||
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // HeaderBackground
|
||||
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // HeaderTitle
|
||||
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // HeaderStatus
|
||||
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // SignalBars
|
||||
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // ConnectionIcon
|
||||
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // UtilizationFill
|
||||
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // FavoriteNode
|
||||
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // ActionMenuBorder
|
||||
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // ActionMenuBody
|
||||
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // ActionMenuTitle
|
||||
{TFTPalette::Black, TFTPalette::MeshtasticGreen}, // FrameMono (bodyBg, bodyFg)
|
||||
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // BootSplash
|
||||
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // FavoriteNodeBGHighlight
|
||||
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // NavigationBar
|
||||
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // NavigationArrow
|
||||
},
|
||||
TFTPalette::Black, // batteryFillGood
|
||||
TFTPalette::Black, // batteryFillMedium
|
||||
TFTPalette::Black, // batteryFillBad
|
||||
true, // fullFrameInvert
|
||||
true, // visible
|
||||
},
|
||||
|
||||
// Classic Red (ThemeID::ClassicRed = 7) classic monochrome
|
||||
{
|
||||
ThemeID::ClassicRed, // id
|
||||
"Classic Red", // name
|
||||
7, // uniqueIdentifier
|
||||
{
|
||||
{TFTPalette::ClassicRed, TFTPalette::Black}, // HeaderBackground
|
||||
{TFTPalette::ClassicRed, TFTPalette::Black}, // HeaderTitle
|
||||
{TFTPalette::ClassicRed, TFTPalette::Black}, // HeaderStatus
|
||||
{TFTPalette::ClassicRed, TFTPalette::Black}, // SignalBars
|
||||
{TFTPalette::ClassicRed, TFTPalette::Black}, // ConnectionIcon
|
||||
{TFTPalette::ClassicRed, TFTPalette::Black}, // UtilizationFill
|
||||
{TFTPalette::ClassicRed, TFTPalette::Black}, // FavoriteNode
|
||||
{TFTPalette::ClassicRed, TFTPalette::Black}, // ActionMenuBorder
|
||||
{TFTPalette::ClassicRed, TFTPalette::Black}, // ActionMenuBody
|
||||
{TFTPalette::ClassicRed, TFTPalette::Black}, // ActionMenuTitle
|
||||
{TFTPalette::Black, TFTPalette::ClassicRed}, // FrameMono (bodyBg, bodyFg)
|
||||
{TFTPalette::ClassicRed, TFTPalette::Black}, // BootSplash
|
||||
{TFTPalette::ClassicRed, TFTPalette::Black}, // FavoriteNodeBGHighlight
|
||||
{TFTPalette::ClassicRed, TFTPalette::Black}, // NavigationBar
|
||||
{TFTPalette::ClassicRed, TFTPalette::Black}, // NavigationArrow
|
||||
},
|
||||
TFTPalette::Black, // batteryFillGood
|
||||
TFTPalette::Black, // batteryFillMedium
|
||||
TFTPalette::Black, // batteryFillBad
|
||||
true, // fullFrameInvert
|
||||
true, // visible
|
||||
},
|
||||
|
||||
// Monochrome White (ThemeID::MonochromeWhite = 8) classic monochrome
|
||||
{
|
||||
ThemeID::MonochromeWhite, // id
|
||||
"Monochrome White", // name
|
||||
8, // uniqueIdentifier
|
||||
{
|
||||
{TFTPalette::White, TFTPalette::Black}, // HeaderBackground
|
||||
{TFTPalette::White, TFTPalette::Black}, // HeaderTitle
|
||||
{TFTPalette::White, TFTPalette::Black}, // HeaderStatus
|
||||
{TFTPalette::White, TFTPalette::Black}, // SignalBars
|
||||
{TFTPalette::White, TFTPalette::Black}, // ConnectionIcon
|
||||
{TFTPalette::White, TFTPalette::Black}, // UtilizationFill
|
||||
{TFTPalette::White, TFTPalette::Black}, // FavoriteNode
|
||||
{TFTPalette::White, TFTPalette::Black}, // ActionMenuBorder
|
||||
{TFTPalette::White, TFTPalette::Black}, // ActionMenuBody
|
||||
{TFTPalette::White, TFTPalette::Black}, // ActionMenuTitle
|
||||
{TFTPalette::Black, TFTPalette::White}, // FrameMono (bodyBg, bodyFg)
|
||||
{TFTPalette::White, TFTPalette::Black}, // BootSplash
|
||||
{TFTPalette::White, TFTPalette::Black}, // FavoriteNodeBGHighlight
|
||||
{TFTPalette::White, TFTPalette::Black}, // NavigationBar
|
||||
{TFTPalette::White, TFTPalette::Black}, // NavigationArrow
|
||||
},
|
||||
TFTPalette::Black, // batteryFillGood
|
||||
TFTPalette::Black, // batteryFillMedium
|
||||
TFTPalette::Black, // batteryFillBad
|
||||
true, // fullFrameInvert
|
||||
true, // visible
|
||||
},
|
||||
};
|
||||
|
||||
static constexpr size_t kInternalThemeCount = sizeof(kThemes) / sizeof(kThemes[0]);
|
||||
|
||||
// Resolve the kThemes[] index for the currently persisted theme. Called at
|
||||
// boot (indirectly via getActiveTheme()) and whenever the active theme is
|
||||
// queried, so uiconfig.screen_rgb_color remains the single source of truth.
|
||||
// Matches against .uniqueIdentifier - that's the field whose value is stored
|
||||
// in the user's config. Falls back to 0 (DefaultDark) if no match is found,
|
||||
// which gracefully handles removed or retired themes.
|
||||
static inline size_t resolveThemeIndex()
|
||||
{
|
||||
const uint32_t savedIdentifier = uiconfig.screen_rgb_color & 0x1F;
|
||||
for (size_t i = 0; i < kInternalThemeCount; i++) {
|
||||
if (kThemes[i].uniqueIdentifier == savedIdentifier)
|
||||
return i;
|
||||
}
|
||||
return 0; // Default Dark fallback
|
||||
}
|
||||
|
||||
static inline bool normalizeRegion(int16_t &x, int16_t &y, int16_t &width, int16_t &height)
|
||||
{
|
||||
if (width <= 0 || height <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (x < 0) {
|
||||
width += x;
|
||||
x = 0;
|
||||
}
|
||||
if (y < 0) {
|
||||
height += y;
|
||||
y = 0;
|
||||
}
|
||||
|
||||
return width > 0 && height > 0;
|
||||
}
|
||||
|
||||
static inline void appendColorRegion(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t onColorBe, uint16_t offColorBe)
|
||||
{
|
||||
// Keep the last slot permanently disabled as a sentinel for ST7789 scans.
|
||||
// This leaves MAX_TFT_COLOR_REGIONS - 1 usable entries.
|
||||
if (colorRegionCount >= MAX_TFT_COLOR_REGIONS - 1) {
|
||||
memmove(&colorRegions[0], &colorRegions[1], sizeof(TFTColorRegion) * (MAX_TFT_COLOR_REGIONS - 2));
|
||||
colorRegionCount = MAX_TFT_COLOR_REGIONS - 2;
|
||||
}
|
||||
|
||||
TFTColorRegion ®ion = colorRegions[colorRegionCount++];
|
||||
region.x = x;
|
||||
region.y = y;
|
||||
region.width = width;
|
||||
region.height = height;
|
||||
region.onColorBe = onColorBe;
|
||||
region.offColorBe = offColorBe;
|
||||
region.enabled = true;
|
||||
|
||||
// Keep one disabled sentinel after the active range for ST7789 countColorRegions().
|
||||
if (colorRegionCount < MAX_TFT_COLOR_REGIONS) {
|
||||
colorRegions[colorRegionCount].enabled = false;
|
||||
}
|
||||
colorRegions[MAX_TFT_COLOR_REGIONS - 1].enabled = false;
|
||||
}
|
||||
|
||||
// Current working role colors (big-endian). Initialised to Dark defaults;
|
||||
// call loadThemeDefaults() after boot / theme change to refresh.
|
||||
static TFTRoleColorsBe roleColors[static_cast<size_t>(TFTColorRole::Count)] = {
|
||||
{toBe565(kHeaderBackground), toBe565(TFTPalette::Black)}, // HeaderBackground
|
||||
{toBe565(kHeaderBackground), toBe565(kTitleColor)}, // HeaderTitle
|
||||
{toBe565(kHeaderBackground), toBe565(kStatusColor)}, // HeaderStatus
|
||||
{toBe565(TFTPalette::Good), toBe565(TFTPalette::Black)}, // SignalBars
|
||||
{toBe565(TFTPalette::Blue), toBe565(TFTPalette::Black)}, // ConnectionIcon
|
||||
{toBe565(TFTPalette::Good), toBe565(TFTPalette::Black)}, // UtilizationFill
|
||||
{toBe565(TFTPalette::Yellow), toBe565(TFTPalette::Black)}, // FavoriteNode
|
||||
{toBe565(TFTPalette::DarkGray), toBe565(TFTPalette::Black)}, // ActionMenuBorder
|
||||
{toBe565(TFTPalette::White), toBe565(TFTPalette::Black)}, // ActionMenuBody
|
||||
{toBe565(TFTPalette::DarkGray), toBe565(TFTPalette::White)}, // ActionMenuTitle
|
||||
{toBe565(TFTPalette::Black), toBe565(TFTPalette::White)}, // FrameMono
|
||||
{toBe565(TFTPalette::White), toBe565(TFTPalette::Black)}, // BootSplash
|
||||
{toBe565(TFTPalette::Yellow), toBe565(TFTPalette::Black)}, // FavoriteNodeBGHighlight
|
||||
{toBe565(kStatusColor), toBe565(kHeaderBackground)}, // NavigationBar
|
||||
{toBe565(kTitleColor), toBe565(TFTPalette::Black)} // NavigationArrow
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// Theme accessors
|
||||
|
||||
const TFTThemeDef &getActiveTheme()
|
||||
{
|
||||
return kThemes[resolveThemeIndex()];
|
||||
}
|
||||
|
||||
// Visible-theme accessors
|
||||
// These iterate only themes flagged .visible = true, preserving kThemes[]
|
||||
// order. Menu code should use these so hidden themes don't appear in the
|
||||
// picker while still applying correctly if their ID is persisted.
|
||||
|
||||
size_t getVisibleThemeCount()
|
||||
{
|
||||
size_t count = 0;
|
||||
for (size_t i = 0; i < kInternalThemeCount; i++) {
|
||||
if (kThemes[i].visible)
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
const TFTThemeDef &getVisibleThemeByIndex(size_t visibleIndex)
|
||||
{
|
||||
size_t seen = 0;
|
||||
for (size_t i = 0; i < kInternalThemeCount; i++) {
|
||||
if (!kThemes[i].visible)
|
||||
continue;
|
||||
if (seen == visibleIndex)
|
||||
return kThemes[i];
|
||||
seen++;
|
||||
}
|
||||
// Fallback: return first theme (never trust a bad index).
|
||||
return kThemes[0];
|
||||
}
|
||||
|
||||
size_t getActiveVisibleThemeIndex()
|
||||
{
|
||||
const size_t active = resolveThemeIndex();
|
||||
if (!kThemes[active].visible)
|
||||
return SIZE_MAX;
|
||||
size_t visibleIdx = 0;
|
||||
for (size_t i = 0; i < active; i++) {
|
||||
if (kThemes[i].visible)
|
||||
visibleIdx++;
|
||||
}
|
||||
return visibleIdx;
|
||||
}
|
||||
|
||||
uint16_t getThemeHeaderBg()
|
||||
{
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
#ifdef TFT_HEADER_BG_COLOR_OVERRIDE
|
||||
return TFT_HEADER_BG_COLOR_OVERRIDE;
|
||||
#else
|
||||
return kThemes[resolveThemeIndex()].roles[static_cast<size_t>(TFTColorRole::HeaderBackground)].onColor;
|
||||
#endif
|
||||
#else
|
||||
return TFTPalette::DarkGray;
|
||||
#endif
|
||||
}
|
||||
|
||||
uint16_t getThemeHeaderText()
|
||||
{
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
#ifdef TFT_HEADER_TITLE_COLOR_OVERRIDE
|
||||
return TFT_HEADER_TITLE_COLOR_OVERRIDE;
|
||||
#else
|
||||
return kThemes[resolveThemeIndex()].roles[static_cast<size_t>(TFTColorRole::HeaderTitle)].offColor;
|
||||
#endif
|
||||
#else
|
||||
return TFTPalette::White;
|
||||
#endif
|
||||
}
|
||||
|
||||
uint16_t getThemeHeaderStatus()
|
||||
{
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
#ifdef TFT_HEADER_STATUS_COLOR_OVERRIDE
|
||||
return TFT_HEADER_STATUS_COLOR_OVERRIDE;
|
||||
#else
|
||||
return kThemes[resolveThemeIndex()].roles[static_cast<size_t>(TFTColorRole::HeaderStatus)].offColor;
|
||||
#endif
|
||||
#else
|
||||
return TFTPalette::White;
|
||||
#endif
|
||||
}
|
||||
|
||||
uint16_t getThemeBodyBg()
|
||||
{
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
return kThemes[resolveThemeIndex()].roles[static_cast<size_t>(TFTColorRole::FrameMono)].onColor;
|
||||
#else
|
||||
return TFTPalette::Black;
|
||||
#endif
|
||||
}
|
||||
|
||||
uint16_t getThemeBodyFg()
|
||||
{
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
return kThemes[resolveThemeIndex()].roles[static_cast<size_t>(TFTColorRole::FrameMono)].offColor;
|
||||
#else
|
||||
return TFTPalette::White;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool isThemeFullFrameInvert()
|
||||
{
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
return kThemes[resolveThemeIndex()].fullFrameInvert;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
uint16_t getThemeBatteryFillColor(int batteryPercent)
|
||||
{
|
||||
const TFTThemeDef &theme = kThemes[resolveThemeIndex()];
|
||||
if (batteryPercent <= 20) {
|
||||
return theme.batteryFillBad;
|
||||
}
|
||||
if (batteryPercent <= 50) {
|
||||
return theme.batteryFillMedium;
|
||||
}
|
||||
return theme.batteryFillGood;
|
||||
}
|
||||
|
||||
void loadThemeDefaults()
|
||||
{
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
const TFTThemeDef &theme = kThemes[resolveThemeIndex()];
|
||||
for (uint8_t i = 0; i < static_cast<uint8_t>(TFTColorRole::Count); i++) {
|
||||
roleColors[i].onColorBe = toBe565(theme.roles[i].onColor);
|
||||
roleColors[i].offColorBe = toBe565(theme.roles[i].offColor);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Role color assignment with theme-aware transforms
|
||||
|
||||
void setTFTColorRole(TFTColorRole role, uint16_t onColor, uint16_t offColor)
|
||||
{
|
||||
#if !GRAPHICS_TFT_COLORING_ENABLED
|
||||
return;
|
||||
#endif
|
||||
|
||||
const uint8_t index = static_cast<uint8_t>(role);
|
||||
if (index >= static_cast<uint8_t>(TFTColorRole::Count)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t themeId = uiconfig.screen_rgb_color & 0x1F;
|
||||
const bool isHighlightRole = (role == TFTColorRole::FavoriteNode || role == TFTColorRole::FavoriteNodeBGHighlight);
|
||||
const bool isBodyRole = !isHighlightRole && isBodyColorRole(role);
|
||||
|
||||
// Classic monochrome themes collapse all non-black accents into one tone.
|
||||
if (isMonochromeTheme(themeId)) {
|
||||
if (onColor != TFTPalette::Black) {
|
||||
onColor = getMonochromeAccent(themeId);
|
||||
}
|
||||
} else {
|
||||
switch (themeId) {
|
||||
case ThemeID::DefaultLight:
|
||||
if (isHighlightRole) {
|
||||
// High-contrast highlight chips on light UI.
|
||||
onColor = TFTPalette::Black;
|
||||
offColor = TFTPalette::Yellow;
|
||||
} else if (isBodyRole) {
|
||||
// Invert body colors for readability on white frames.
|
||||
if (offColor == TFTPalette::Black && role != TFTColorRole::ActionMenuTitle) {
|
||||
offColor = TFTPalette::White;
|
||||
}
|
||||
replaceColor(onColor, TFTPalette::White, TFTPalette::Black);
|
||||
}
|
||||
break;
|
||||
case ThemeID::Christmas:
|
||||
if (isHighlightRole || isBodyRole) {
|
||||
replaceColor(onColor, TFTPalette::Yellow, TFTPalette::Gold);
|
||||
replaceColor(offColor, TFTPalette::Black, TFTPalette::Pine);
|
||||
}
|
||||
break;
|
||||
case ThemeID::Pink:
|
||||
if (isHighlightRole) {
|
||||
onColor = TFTPalette::Black;
|
||||
offColor = TFTPalette::HotPink;
|
||||
} else if (isBodyRole) {
|
||||
replaceColor(offColor, TFTPalette::Black, TFTPalette::PalePink);
|
||||
replaceColor(onColor, TFTPalette::White, TFTPalette::Black);
|
||||
replaceColor(onColor, TFTPalette::Yellow, TFTPalette::DeepPink);
|
||||
}
|
||||
break;
|
||||
case ThemeID::Creamsicle:
|
||||
if (isHighlightRole) {
|
||||
onColor = TFTPalette::Black;
|
||||
offColor = TFTPalette::CreamOrange;
|
||||
} else if (isBodyRole) {
|
||||
replaceColor(offColor, TFTPalette::Black, TFTPalette::Cream);
|
||||
replaceColor(onColor, TFTPalette::White, TFTPalette::Black);
|
||||
replaceColor(onColor, TFTPalette::Yellow, TFTPalette::DeepOrange);
|
||||
}
|
||||
break;
|
||||
case ThemeID::Blue:
|
||||
if (isHighlightRole || isBodyRole) {
|
||||
replaceColor(onColor, TFTPalette::Yellow, TFTPalette::SkyBlue);
|
||||
replaceColor(offColor, TFTPalette::Black, TFTPalette::Navy);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
roleColors[index].onColorBe = toBe565(onColor);
|
||||
roleColors[index].offColorBe = toBe565(offColor);
|
||||
}
|
||||
|
||||
// Region registration
|
||||
|
||||
void registerTFTColorRegion(TFTColorRole role, int16_t x, int16_t y, int16_t width, int16_t height)
|
||||
{
|
||||
#if !GRAPHICS_TFT_COLORING_ENABLED
|
||||
return;
|
||||
#endif
|
||||
|
||||
const uint8_t roleIndex = static_cast<uint8_t>(role);
|
||||
if (roleIndex >= static_cast<uint8_t>(TFTColorRole::Count)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!normalizeRegion(x, y, width, height)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const TFTRoleColorsBe &colors = roleColors[roleIndex];
|
||||
appendColorRegion(x, y, width, height, colors.onColorBe, colors.offColorBe);
|
||||
}
|
||||
|
||||
void setAndRegisterTFTColorRole(TFTColorRole role, uint16_t onColor, uint16_t offColor, int16_t x, int16_t y, int16_t width,
|
||||
int16_t height)
|
||||
{
|
||||
#if !GRAPHICS_TFT_COLORING_ENABLED
|
||||
(void)role;
|
||||
(void)onColor;
|
||||
(void)offColor;
|
||||
(void)x;
|
||||
(void)y;
|
||||
(void)width;
|
||||
(void)height;
|
||||
return;
|
||||
#else
|
||||
setTFTColorRole(role, onColor, offColor);
|
||||
registerTFTColorRegion(role, x, y, width, height);
|
||||
#endif
|
||||
}
|
||||
|
||||
void registerTFTColorRegionDirect(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t onColor, uint16_t offColor)
|
||||
{
|
||||
#if !GRAPHICS_TFT_COLORING_ENABLED
|
||||
return;
|
||||
#endif
|
||||
|
||||
if (!normalizeRegion(x, y, width, height))
|
||||
return;
|
||||
|
||||
appendColorRegion(x, y, width, height, toBe565(onColor), toBe565(offColor));
|
||||
}
|
||||
|
||||
void registerTFTActionMenuRegions(int16_t boxLeft, int16_t boxTop, int16_t boxWidth, int16_t boxHeight)
|
||||
{
|
||||
#if !GRAPHICS_TFT_COLORING_ENABLED
|
||||
(void)boxLeft;
|
||||
(void)boxTop;
|
||||
(void)boxWidth;
|
||||
(void)boxHeight;
|
||||
return;
|
||||
#else
|
||||
// Use theme-appropriate menu colors.
|
||||
const TFTThemeDef &theme = kThemes[resolveThemeIndex()];
|
||||
const TFTThemeRoleColor &menuBody = theme.roles[static_cast<size_t>(TFTColorRole::ActionMenuBody)];
|
||||
const TFTThemeRoleColor &menuBorder = theme.roles[static_cast<size_t>(TFTColorRole::ActionMenuBorder)];
|
||||
|
||||
// Fill role includes a 1px shadow guard so stale frame edges are overwritten uniformly.
|
||||
setAndRegisterTFTColorRole(TFTColorRole::ActionMenuBody, menuBody.onColor, menuBody.offColor, boxLeft - 1, boxTop - 1,
|
||||
boxWidth + 2, boxHeight + 2);
|
||||
registerTFTColorRegion(TFTColorRole::ActionMenuBody, boxLeft, boxTop - 2, boxWidth, 1);
|
||||
registerTFTColorRegion(TFTColorRole::ActionMenuBody, boxLeft, boxTop + boxHeight + 1, boxWidth, 1);
|
||||
registerTFTColorRegion(TFTColorRole::ActionMenuBody, boxLeft - 2, boxTop, 1, boxHeight);
|
||||
registerTFTColorRegion(TFTColorRole::ActionMenuBody, boxLeft + boxWidth + 1, boxTop, 1, boxHeight);
|
||||
|
||||
setAndRegisterTFTColorRole(TFTColorRole::ActionMenuBorder, menuBorder.onColor, menuBorder.offColor, boxLeft, boxTop, boxWidth,
|
||||
1);
|
||||
registerTFTColorRegion(TFTColorRole::ActionMenuBorder, boxLeft, boxTop + boxHeight - 1, boxWidth, 1);
|
||||
registerTFTColorRegion(TFTColorRole::ActionMenuBorder, boxLeft, boxTop, 1, boxHeight);
|
||||
registerTFTColorRegion(TFTColorRole::ActionMenuBorder, boxLeft + boxWidth - 1, boxTop, 1, boxHeight);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Frame signature & utilities
|
||||
|
||||
uint32_t getTFTColorFrameSignature()
|
||||
{
|
||||
#if !GRAPHICS_TFT_COLORING_ENABLED
|
||||
return 0;
|
||||
#else
|
||||
uint32_t hash = kFnv1aOffsetBasis;
|
||||
hash = fnv1aAppendByte(hash, colorRegionCount);
|
||||
for (uint8_t i = 0; i < colorRegionCount; i++) {
|
||||
const TFTColorRegion &r = colorRegions[i];
|
||||
hash = fnv1aAppendU16(hash, static_cast<uint16_t>(r.x));
|
||||
hash = fnv1aAppendU16(hash, static_cast<uint16_t>(r.y));
|
||||
hash = fnv1aAppendU16(hash, static_cast<uint16_t>(r.width));
|
||||
hash = fnv1aAppendU16(hash, static_cast<uint16_t>(r.height));
|
||||
hash = fnv1aAppendU16(hash, r.onColorBe);
|
||||
hash = fnv1aAppendU16(hash, r.offColorBe);
|
||||
}
|
||||
|
||||
return hash;
|
||||
#endif
|
||||
}
|
||||
|
||||
uint8_t getTFTColorRegionCount()
|
||||
{
|
||||
#if !GRAPHICS_TFT_COLORING_ENABLED
|
||||
return 0;
|
||||
#else
|
||||
return colorRegionCount;
|
||||
#endif
|
||||
}
|
||||
|
||||
void clearTFTColorRegions()
|
||||
{
|
||||
for (uint8_t i = 0; i < colorRegionCount; i++) {
|
||||
colorRegions[i].enabled = false;
|
||||
}
|
||||
if (colorRegionCount < MAX_TFT_COLOR_REGIONS) {
|
||||
colorRegions[colorRegionCount].enabled = false;
|
||||
}
|
||||
colorRegionCount = 0;
|
||||
}
|
||||
|
||||
uint16_t resolveTFTColorPixel(int16_t x, int16_t y, bool isset, uint16_t defaultOnColor, uint16_t defaultOffColor)
|
||||
{
|
||||
for (int i = static_cast<int>(colorRegionCount) - 1; i >= 0; i--) {
|
||||
const TFTColorRegion &r = colorRegions[i];
|
||||
if (x >= r.x && x < r.x + r.width && y >= r.y && y < r.y + r.height) {
|
||||
return isset ? r.onColorBe : r.offColorBe;
|
||||
}
|
||||
}
|
||||
return isset ? defaultOnColor : defaultOffColor;
|
||||
}
|
||||
|
||||
uint16_t resolveTFTOffColorAt(int16_t x, int16_t y, uint16_t defaultOffColor)
|
||||
{
|
||||
#if !GRAPHICS_TFT_COLORING_ENABLED
|
||||
(void)x;
|
||||
(void)y;
|
||||
return defaultOffColor;
|
||||
#else
|
||||
const uint16_t defaultOffBe = toBe565(defaultOffColor);
|
||||
const uint16_t sampledBe = resolveTFTColorPixel(x, y, false, defaultOffBe, defaultOffBe);
|
||||
return static_cast<uint16_t>((sampledBe >> 8) | (sampledBe << 8));
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace graphics
|
||||
@@ -0,0 +1,163 @@
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
#include <stdint.h>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
|
||||
struct TFTColorRegion {
|
||||
int16_t x;
|
||||
int16_t y;
|
||||
int16_t width;
|
||||
int16_t height;
|
||||
uint16_t onColorBe;
|
||||
uint16_t offColorBe;
|
||||
// Required by ST7789 driver: it scans until the first disabled entry.
|
||||
bool enabled = false;
|
||||
};
|
||||
|
||||
static constexpr size_t MAX_TFT_COLOR_REGIONS = 48;
|
||||
extern TFTColorRegion colorRegions[MAX_TFT_COLOR_REGIONS];
|
||||
|
||||
enum class TFTColorRole : uint8_t {
|
||||
HeaderBackground = 0,
|
||||
HeaderTitle,
|
||||
HeaderStatus,
|
||||
SignalBars,
|
||||
ConnectionIcon,
|
||||
UtilizationFill,
|
||||
FavoriteNode,
|
||||
ActionMenuBorder,
|
||||
ActionMenuBody,
|
||||
ActionMenuTitle,
|
||||
FrameMono,
|
||||
BootSplash,
|
||||
FavoriteNodeBGHighlight,
|
||||
NavigationBar,
|
||||
NavigationArrow,
|
||||
Count
|
||||
};
|
||||
|
||||
#if HAS_TFT || defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \
|
||||
defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || \
|
||||
defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR)
|
||||
#define GRAPHICS_TFT_COLORING_ENABLED 1
|
||||
#else
|
||||
#define GRAPHICS_TFT_COLORING_ENABLED 0
|
||||
#endif
|
||||
|
||||
static constexpr bool kTFTColoringEnabled = GRAPHICS_TFT_COLORING_ENABLED != 0;
|
||||
constexpr bool isTFTColoringEnabled()
|
||||
{
|
||||
return kTFTColoringEnabled;
|
||||
}
|
||||
|
||||
void setTFTColorRole(TFTColorRole role, uint16_t onColor, uint16_t offColor);
|
||||
void registerTFTColorRegion(TFTColorRole role, int16_t x, int16_t y, int16_t width, int16_t height);
|
||||
// Convenience helper for the common "set role then register one region" flow.
|
||||
void setAndRegisterTFTColorRole(TFTColorRole role, uint16_t onColor, uint16_t offColor, int16_t x, int16_t y, int16_t width,
|
||||
int16_t height);
|
||||
// Register a region using explicit colors (no role lookup). Use when the
|
||||
// color comes from a theme field rather than a role (e.g. battery fill).
|
||||
void registerTFTColorRegionDirect(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t onColor, uint16_t offColor);
|
||||
void registerTFTActionMenuRegions(int16_t boxLeft, int16_t boxTop, int16_t boxWidth, int16_t boxHeight);
|
||||
uint32_t getTFTColorFrameSignature();
|
||||
uint8_t getTFTColorRegionCount();
|
||||
void clearTFTColorRegions();
|
||||
uint16_t resolveTFTColorPixel(int16_t x, int16_t y, bool isset, uint16_t defaultOnColor, uint16_t defaultOffColor);
|
||||
// Resolve effective region-mapped OFF color at a coordinate in native-endian RGB565.
|
||||
uint16_t resolveTFTOffColorAt(int16_t x, int16_t y, uint16_t defaultOffColor);
|
||||
|
||||
// -- Theme engine ------------------------------------------------------
|
||||
// Each theme has four fields that work together:
|
||||
//
|
||||
// id - ThemeID:: constant, used for in-code references.
|
||||
// name - human-readable label shown in the theme picker.
|
||||
// uniqueIdentifier - the stable numeric value persisted to
|
||||
// uiconfig.screen_rgb_color and restored at boot.
|
||||
// This is a CONTRACT with saved configs on disk - once
|
||||
// assigned, never reuse or renumber, even if the theme is
|
||||
// deleted or the kThemes[] array is reordered.
|
||||
// visible - controls whether a theme appears in the picker menu.
|
||||
// Hidden themes can still be restored and applied if their
|
||||
// uniqueIdentifier is persisted.
|
||||
//
|
||||
// Display order in the menu is controlled by kThemes[] array order among
|
||||
// themes where visible == true, NOT by any numeric value above.
|
||||
//
|
||||
// To add a new theme:
|
||||
// 1. Add a unique constant in ThemeID below (next unused value).
|
||||
// 2. Add a kThemes[] entry at the desired menu position, with a unique
|
||||
// uniqueIdentifier that has never been used by any prior theme.
|
||||
// 3. Set visible=true if it should appear in the picker.
|
||||
//
|
||||
// To retire a theme without breaking saved configs:
|
||||
// - Preferred: keep the entry and set visible=false so existing saved
|
||||
// uniqueIdentifier values still resolve to the same theme.
|
||||
// - If you remove the entry, resolveThemeIndex() falls back to DefaultDark
|
||||
// when the persisted uniqueIdentifier no longer matches any theme.
|
||||
// - Do NOT reuse a retired uniqueIdentifier for a future theme.
|
||||
namespace ThemeID
|
||||
{
|
||||
constexpr uint32_t DefaultDark = 0;
|
||||
constexpr uint32_t DefaultLight = 1;
|
||||
constexpr uint32_t Christmas = 2;
|
||||
constexpr uint32_t Pink = 3;
|
||||
constexpr uint32_t Blue = 4;
|
||||
constexpr uint32_t Creamsicle = 5;
|
||||
constexpr uint32_t MeshtasticGreen = 6;
|
||||
constexpr uint32_t ClassicRed = 7;
|
||||
constexpr uint32_t MonochromeWhite = 8;
|
||||
} // namespace ThemeID
|
||||
|
||||
// Per-role color pair stored in native (little-endian) RGB565 format.
|
||||
struct TFTThemeRoleColor {
|
||||
uint16_t onColor;
|
||||
uint16_t offColor;
|
||||
};
|
||||
|
||||
// Complete theme definition.
|
||||
struct TFTThemeDef {
|
||||
uint32_t id; // ThemeID constant - in-code identifier for this theme.
|
||||
const char *name; // Human-readable label shown in the theme picker.
|
||||
uint32_t uniqueIdentifier; // Stable persisted value copied into uiconfig.screen_rgb_color.
|
||||
// Never reuse or renumber - see file-level notes above.
|
||||
TFTThemeRoleColor roles[static_cast<size_t>(TFTColorRole::Count)];
|
||||
uint16_t batteryFillGood;
|
||||
uint16_t batteryFillMedium;
|
||||
uint16_t batteryFillBad;
|
||||
bool fullFrameInvert; // Apply full-frame FrameMono inversion (ST7789 light themes)
|
||||
bool visible; // Show in the theme picker menu. Hidden themes still apply
|
||||
// correctly if their uniqueIdentifier is persisted (dev/legacy themes).
|
||||
};
|
||||
|
||||
// Count of themes whose .visible flag is true. Use this when building menus.
|
||||
size_t getVisibleThemeCount();
|
||||
|
||||
// Access the Nth visible theme (0 .. getVisibleThemeCount()-1). Hidden themes
|
||||
// are skipped, preserving kThemes[] order among the visible entries.
|
||||
const TFTThemeDef &getVisibleThemeByIndex(size_t visibleIndex);
|
||||
|
||||
// Return the theme that matches uiconfig.screen_rgb_color (falls back to Dark).
|
||||
const TFTThemeDef &getActiveTheme();
|
||||
|
||||
// Return the visible-theme index for the currently active theme, or SIZE_MAX
|
||||
// if the active theme is hidden (so menus can show "no selection").
|
||||
size_t getActiveVisibleThemeIndex();
|
||||
|
||||
// Convenience accessors - safe to call even when coloring is compiled out.
|
||||
uint16_t getThemeHeaderBg();
|
||||
uint16_t getThemeHeaderText();
|
||||
uint16_t getThemeHeaderStatus();
|
||||
uint16_t getThemeBodyBg();
|
||||
uint16_t getThemeBodyFg();
|
||||
bool isThemeFullFrameInvert();
|
||||
uint16_t getThemeBatteryFillColor(int batteryPercent);
|
||||
|
||||
// Reinitialise default roleColors from the active theme. Call after a
|
||||
// theme change so that any role registered without a prior setTFTColorRole()
|
||||
// picks up theme-appropriate defaults.
|
||||
void loadThemeDefaults();
|
||||
|
||||
} // namespace graphics
|
||||
+145
-38
@@ -16,12 +16,6 @@
|
||||
extern SX1509 gpioExtender;
|
||||
#endif
|
||||
|
||||
#ifdef TFT_MESH_OVERRIDE
|
||||
uint16_t TFT_MESH = TFT_MESH_OVERRIDE;
|
||||
#else
|
||||
uint16_t TFT_MESH = COLOR565(0x67, 0xEA, 0x94);
|
||||
#endif
|
||||
|
||||
#if defined(ST7735S)
|
||||
#include <LovyanGFX.hpp> // Graphics and font library for ST7735 driver chip
|
||||
|
||||
@@ -1151,7 +1145,9 @@ static LGFX *tft = nullptr;
|
||||
#endif
|
||||
|
||||
#include "SPILock.h"
|
||||
#include "TFTColorRegions.h"
|
||||
#include "TFTDisplay.h"
|
||||
#include "TFTPalette.h"
|
||||
#include <SPI.h>
|
||||
|
||||
#ifdef UNPHONE
|
||||
@@ -1161,6 +1157,25 @@ extern unPhone unphone;
|
||||
|
||||
GpioPin *TFTDisplay::backlightEnable = NULL;
|
||||
|
||||
namespace
|
||||
{
|
||||
static constexpr uint8_t kFullRepaintChunkRows = 8;
|
||||
|
||||
static inline uint16_t getThemeDefaultOnColor()
|
||||
{
|
||||
return graphics::TFTPalette::White;
|
||||
}
|
||||
|
||||
static inline uint16_t getThemeDefaultOffColor()
|
||||
{
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
return graphics::getThemeBodyBg();
|
||||
#else
|
||||
return TFT_BLACK;
|
||||
#endif
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TFTDisplay::TFTDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus)
|
||||
{
|
||||
LOG_DEBUG("TFTDisplay!");
|
||||
@@ -1200,14 +1215,15 @@ TFTDisplay::~TFTDisplay()
|
||||
free(linePixelBuffer);
|
||||
linePixelBuffer = nullptr;
|
||||
}
|
||||
if (repaintChunkBuffer != nullptr) {
|
||||
free(repaintChunkBuffer);
|
||||
repaintChunkBuffer = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// Write the buffer to the display memory
|
||||
void TFTDisplay::display(bool fromBlank)
|
||||
{
|
||||
if (fromBlank)
|
||||
tft->fillScreen(TFT_BLACK);
|
||||
|
||||
concurrency::LockGuard g(spiLock);
|
||||
|
||||
uint32_t x, y;
|
||||
@@ -1216,12 +1232,70 @@ void TFTDisplay::display(bool fromBlank)
|
||||
uint32_t x_FirstPixelUpdate;
|
||||
uint32_t x_LastPixelUpdate;
|
||||
bool isset, dblbuf_isset;
|
||||
uint16_t colorTftMesh, colorTftBlack;
|
||||
uint16_t colorTftWhite, colorTftBlack;
|
||||
bool somethingChanged = false;
|
||||
|
||||
// Store colors byte-reversed so that TFT_eSPI doesn't have to swap bytes in a separate step
|
||||
colorTftMesh = __builtin_bswap16(TFT_MESH);
|
||||
colorTftBlack = __builtin_bswap16(TFT_BLACK);
|
||||
// Theme defaults for non-role pixels.
|
||||
const uint16_t defaultOnColor = getThemeDefaultOnColor();
|
||||
const uint16_t defaultOffColor = getThemeDefaultOffColor();
|
||||
static uint16_t lastDefaultOnColor = 0;
|
||||
static uint16_t lastDefaultOffColor = 0;
|
||||
static bool haveLastDefaults = false;
|
||||
const bool themeDefaultsChanged =
|
||||
!haveLastDefaults || (defaultOnColor != lastDefaultOnColor) || (defaultOffColor != lastDefaultOffColor);
|
||||
const bool forceFullRepaint = fromBlank || themeDefaultsChanged;
|
||||
|
||||
// If theme defaults changed, reset panel background immediately so stale pixels don't linger.
|
||||
if (forceFullRepaint) {
|
||||
tft->fillScreen(defaultOffColor);
|
||||
}
|
||||
|
||||
colorTftWhite = (defaultOnColor >> 8) | ((defaultOnColor & 0xFF) << 8);
|
||||
colorTftBlack = (defaultOffColor >> 8) | ((defaultOffColor & 0xFF) << 8);
|
||||
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
static uint32_t lastColorFrameSignature = 0;
|
||||
const bool hasColorRegions = graphics::getTFTColorRegionCount() > 0;
|
||||
const uint32_t colorFrameSignature = graphics::getTFTColorFrameSignature();
|
||||
const bool forceFullColorRepaint = forceFullRepaint || (colorFrameSignature != lastColorFrameSignature);
|
||||
|
||||
// When region roles/layout changed, color can differ even with identical monochrome glyph bits.
|
||||
// Repaint full frame only for those frames, then return to diff-based updates.
|
||||
if (forceFullColorRepaint) {
|
||||
for (uint32_t yStart = 0; yStart < displayHeight; yStart += kFullRepaintChunkRows) {
|
||||
const uint32_t rowsThisChunk = min<uint32_t>(kFullRepaintChunkRows, displayHeight - yStart);
|
||||
for (uint32_t row = 0; row < rowsThisChunk; row++) {
|
||||
y = yStart + row;
|
||||
y_byteIndex = (y / 8) * displayWidth;
|
||||
y_byteMask = (1 << (y & 7));
|
||||
|
||||
uint16_t *chunkRow = repaintChunkBuffer + (row * displayWidth);
|
||||
for (x = 0; x < displayWidth; x++) {
|
||||
isset = (buffer[x + y_byteIndex] & y_byteMask) != 0;
|
||||
if (hasColorRegions) {
|
||||
chunkRow[x] = graphics::resolveTFTColorPixel(static_cast<int16_t>(x), static_cast<int16_t>(y), isset,
|
||||
colorTftWhite, colorTftBlack);
|
||||
} else {
|
||||
chunkRow[x] = isset ? colorTftWhite : colorTftBlack;
|
||||
}
|
||||
}
|
||||
}
|
||||
#if defined(HACKADAY_COMMUNICATOR)
|
||||
tft->draw16bitBeRGBBitmap(0, yStart, repaintChunkBuffer, displayWidth, rowsThisChunk);
|
||||
#else
|
||||
tft->pushImage(0, yStart, displayWidth, rowsThisChunk, repaintChunkBuffer);
|
||||
#endif
|
||||
}
|
||||
|
||||
memcpy(buffer_back, buffer, displayBufferSize);
|
||||
lastColorFrameSignature = colorFrameSignature;
|
||||
haveLastDefaults = true;
|
||||
lastDefaultOnColor = defaultOnColor;
|
||||
lastDefaultOffColor = defaultOffColor;
|
||||
graphics::clearTFTColorRegions();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
y = 0;
|
||||
while (y < displayHeight) {
|
||||
@@ -1230,7 +1304,7 @@ void TFTDisplay::display(bool fromBlank)
|
||||
|
||||
// Step 1: Do a quick scan of 8 rows together. This allows fast-forwarding over unchanged screen areas.
|
||||
if (y_byteMask == 1) {
|
||||
if (!fromBlank) {
|
||||
if (!forceFullRepaint) {
|
||||
for (x = 0; x < displayWidth; x++) {
|
||||
if (buffer[x + y_byteIndex] != buffer_back[x + y_byteIndex])
|
||||
break;
|
||||
@@ -1248,13 +1322,14 @@ void TFTDisplay::display(bool fromBlank)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Scan each of the 8 rows individually. Find the first pixel in each row that needs updating
|
||||
for (x_FirstPixelUpdate = 0; x_FirstPixelUpdate < displayWidth; x_FirstPixelUpdate++) {
|
||||
isset = buffer[x_FirstPixelUpdate + y_byteIndex] & y_byteMask;
|
||||
// Step 2: Scan this row for changed span (first and last changed pixel).
|
||||
uint32_t x_FirstChanged = 0;
|
||||
for (x_FirstChanged = 0; x_FirstChanged < displayWidth; x_FirstChanged++) {
|
||||
isset = buffer[x_FirstChanged + y_byteIndex] & y_byteMask;
|
||||
|
||||
if (!fromBlank) {
|
||||
if (!forceFullRepaint) {
|
||||
// get src pixel in the page based ordering the OLED lib uses
|
||||
dblbuf_isset = buffer_back[x_FirstPixelUpdate + y_byteIndex] & y_byteMask;
|
||||
dblbuf_isset = buffer_back[x_FirstChanged + y_byteIndex] & y_byteMask;
|
||||
if (isset != dblbuf_isset) {
|
||||
break;
|
||||
}
|
||||
@@ -1264,26 +1339,42 @@ void TFTDisplay::display(bool fromBlank)
|
||||
}
|
||||
|
||||
// Did we find a pixel that needs updating on this row?
|
||||
if (x_FirstPixelUpdate < displayWidth) {
|
||||
|
||||
// Quickly write out the first changed pixel (saves another array lookup)
|
||||
linePixelBuffer[x_FirstPixelUpdate] = isset ? colorTftMesh : colorTftBlack;
|
||||
x_LastPixelUpdate = x_FirstPixelUpdate;
|
||||
|
||||
// Step 3: copy all remaining pixels in this row into the pixel line buffer,
|
||||
// while also recording the last pixel in the row that needs updating
|
||||
for (x = x_FirstPixelUpdate + 1; x < displayWidth; x++) {
|
||||
isset = buffer[x + y_byteIndex] & y_byteMask;
|
||||
linePixelBuffer[x] = isset ? colorTftMesh : colorTftBlack;
|
||||
|
||||
if (!fromBlank) {
|
||||
dblbuf_isset = buffer_back[x + y_byteIndex] & y_byteMask;
|
||||
if (x_FirstChanged < displayWidth) {
|
||||
uint32_t x_LastChanged = displayWidth - 1;
|
||||
while (x_LastChanged > x_FirstChanged) {
|
||||
isset = buffer[x_LastChanged + y_byteIndex] & y_byteMask;
|
||||
if (!forceFullRepaint) {
|
||||
dblbuf_isset = buffer_back[x_LastChanged + y_byteIndex] & y_byteMask;
|
||||
if (isset != dblbuf_isset) {
|
||||
x_LastPixelUpdate = x;
|
||||
break;
|
||||
}
|
||||
} else if (isset) {
|
||||
x_LastPixelUpdate = x;
|
||||
break;
|
||||
}
|
||||
x_LastChanged--;
|
||||
}
|
||||
|
||||
// Align the first pixel for update to an even number so the total alignment of
|
||||
// the data will be at 32-bit boundary, which is required by GDMA SPI transfers.
|
||||
x_FirstPixelUpdate = x_FirstChanged & ~1U;
|
||||
x_LastPixelUpdate = x_LastChanged | 1U;
|
||||
if (x_LastPixelUpdate >= displayWidth) {
|
||||
x_LastPixelUpdate = displayWidth - 1;
|
||||
}
|
||||
|
||||
// Step 3: Copy only the changed span into the pixel line buffer.
|
||||
for (x = x_FirstPixelUpdate; x <= x_LastPixelUpdate; x++) {
|
||||
isset = buffer[x + y_byteIndex] & y_byteMask;
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
if (hasColorRegions) {
|
||||
linePixelBuffer[x] = graphics::resolveTFTColorPixel(static_cast<int16_t>(x), static_cast<int16_t>(y), isset,
|
||||
colorTftWhite, colorTftBlack);
|
||||
} else {
|
||||
linePixelBuffer[x] = isset ? colorTftWhite : colorTftBlack;
|
||||
}
|
||||
#else
|
||||
linePixelBuffer[x] = isset ? colorTftWhite : colorTftBlack;
|
||||
#endif
|
||||
}
|
||||
#if defined(HACKADAY_COMMUNICATOR)
|
||||
tft->draw16bitBeRGBBitmap(x_FirstPixelUpdate, y, &linePixelBuffer[x_FirstPixelUpdate],
|
||||
@@ -1291,8 +1382,8 @@ void TFTDisplay::display(bool fromBlank)
|
||||
#else
|
||||
// Step 4: Send the changed pixels on this line to the screen as a single block transfer.
|
||||
// This function accepts pixel data MSB first so it can dump the memory straight out the SPI port.
|
||||
tft->pushRect(x_FirstPixelUpdate, y, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1,
|
||||
&linePixelBuffer[x_FirstPixelUpdate]);
|
||||
tft->pushImage(x_FirstPixelUpdate, y, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1,
|
||||
&linePixelBuffer[x_FirstPixelUpdate]);
|
||||
#endif
|
||||
somethingChanged = true;
|
||||
}
|
||||
@@ -1301,6 +1392,14 @@ void TFTDisplay::display(bool fromBlank)
|
||||
// Copy the Buffer to the Back Buffer
|
||||
if (somethingChanged)
|
||||
memcpy(buffer_back, buffer, displayBufferSize);
|
||||
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
lastColorFrameSignature = colorFrameSignature;
|
||||
#endif
|
||||
haveLastDefaults = true;
|
||||
lastDefaultOnColor = defaultOnColor;
|
||||
lastDefaultOffColor = defaultOffColor;
|
||||
graphics::clearTFTColorRegions();
|
||||
}
|
||||
|
||||
void TFTDisplay::sdlLoop()
|
||||
@@ -1514,7 +1613,7 @@ bool TFTDisplay::connect()
|
||||
#else
|
||||
tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label
|
||||
#endif
|
||||
tft->fillScreen(TFT_BLACK);
|
||||
tft->fillScreen(getThemeDefaultOffColor());
|
||||
|
||||
if (this->linePixelBuffer == NULL) {
|
||||
this->linePixelBuffer = (uint16_t *)malloc(sizeof(uint16_t) * displayWidth);
|
||||
@@ -1524,6 +1623,14 @@ bool TFTDisplay::connect()
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (this->repaintChunkBuffer == NULL) {
|
||||
this->repaintChunkBuffer = (uint16_t *)malloc(sizeof(uint16_t) * displayWidth * kFullRepaintChunkRows);
|
||||
|
||||
if (!this->repaintChunkBuffer) {
|
||||
LOG_ERROR("Not enough memory to create TFT repaint chunk buffer\n");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -63,4 +63,5 @@ class TFTDisplay : public OLEDDisplay
|
||||
virtual bool connect() override;
|
||||
|
||||
uint16_t *linePixelBuffer = nullptr;
|
||||
};
|
||||
uint16_t *repaintChunkBuffer = nullptr;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
namespace TFTPalette
|
||||
{
|
||||
|
||||
constexpr uint16_t rgb565(uint8_t red, uint8_t green, uint8_t blue)
|
||||
{
|
||||
return static_cast<uint16_t>(((red & 0xF8) << 8) | ((green & 0xFC) << 3) | ((blue & 0xF8) >> 3));
|
||||
}
|
||||
|
||||
constexpr uint16_t Black = 0x0000;
|
||||
constexpr uint16_t White = 0xFFFF;
|
||||
constexpr uint16_t DarkGray = 0x4208;
|
||||
constexpr uint16_t Gray = 0x8410;
|
||||
constexpr uint16_t LightGray = 0xC618;
|
||||
|
||||
constexpr uint16_t Red = rgb565(255, 0, 0);
|
||||
constexpr uint16_t Green = rgb565(0, 255, 0);
|
||||
constexpr uint16_t Blue = rgb565(0, 130, 252);
|
||||
constexpr uint16_t Yellow = rgb565(255, 255, 0);
|
||||
constexpr uint16_t Orange = rgb565(255, 165, 0);
|
||||
constexpr uint16_t Cyan = rgb565(0, 255, 255);
|
||||
constexpr uint16_t Magenta = rgb565(255, 0, 255);
|
||||
|
||||
constexpr uint16_t Good = Green;
|
||||
constexpr uint16_t Medium = Yellow;
|
||||
constexpr uint16_t Bad = Red;
|
||||
|
||||
// Christmas / seasonal accent colors
|
||||
constexpr uint16_t ChristmasRed = rgb565(178, 34, 34);
|
||||
constexpr uint16_t ChristmasGreen = rgb565(0, 128, 0);
|
||||
constexpr uint16_t Gold = rgb565(255, 215, 0);
|
||||
constexpr uint16_t Pine = rgb565(15, 35, 10);
|
||||
|
||||
// Pink theme colors (light variant)
|
||||
constexpr uint16_t HotPink = rgb565(255, 105, 180);
|
||||
constexpr uint16_t PalePink = rgb565(255, 228, 235);
|
||||
constexpr uint16_t DeepPink = rgb565(200, 50, 120);
|
||||
|
||||
// Blue theme colors (dark variant)
|
||||
constexpr uint16_t SkyBlue = rgb565(100, 180, 255);
|
||||
constexpr uint16_t Navy = rgb565(15, 15, 50);
|
||||
constexpr uint16_t DeepBlue = rgb565(30, 60, 120);
|
||||
|
||||
// Creamsicle theme colors (light variant)
|
||||
constexpr uint16_t CreamOrange = rgb565(255, 140, 50);
|
||||
constexpr uint16_t DeepOrange = rgb565(220, 100, 20);
|
||||
constexpr uint16_t Cream = rgb565(255, 248, 235);
|
||||
|
||||
// Classic monochrome theme accent colors (single-color-on-black themes)
|
||||
constexpr uint16_t MeshtasticGreen = rgb565(0x67, 0xEA, 0x94);
|
||||
constexpr uint16_t ClassicRed = rgb565(255, 64, 64);
|
||||
// Monochrome White reuses TFTPalette::White above.
|
||||
|
||||
// Fast contrast picker for monochrome glyph overlays on arbitrary RGB565 backgrounds.
|
||||
// Uses channel-sum brightness approximation to keep code size small.
|
||||
constexpr uint16_t pickReadableMonoFg(uint16_t backgroundColor)
|
||||
{
|
||||
const uint16_t r = (backgroundColor >> 11) & 0x1F;
|
||||
const uint16_t g = (backgroundColor >> 5) & 0x3F;
|
||||
const uint16_t b = backgroundColor & 0x1F;
|
||||
return ((r + g + b) >= 70) ? DarkGray : White;
|
||||
}
|
||||
|
||||
} // namespace TFTPalette
|
||||
} // namespace graphics
|
||||
@@ -145,7 +145,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
|
||||
// === Set Title, Blank for Clock
|
||||
const char *titleStr = "";
|
||||
// === Header ===
|
||||
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
|
||||
graphics::drawCommonHeader(display, x, y, titleStr, true, true, true);
|
||||
|
||||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
|
||||
char timeString[16];
|
||||
@@ -293,11 +293,15 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
|
||||
// Draw an analog clock
|
||||
void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
// Clear previous frame pixels so moving hands don't leave stale artifacts on TFT light theme.
|
||||
display->clear();
|
||||
#endif
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
// === Set Title, Blank for Clock
|
||||
const char *titleStr = "";
|
||||
// === Header ===
|
||||
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
|
||||
graphics::drawCommonHeader(display, x, y, titleStr, true, true, true);
|
||||
|
||||
// clock face center coordinates
|
||||
int16_t centerX = display->getWidth() / 2;
|
||||
@@ -478,4 +482,4 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
} // namespace ClockRenderer
|
||||
|
||||
} // namespace graphics
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
#include "configuration.h"
|
||||
#if HAS_SCREEN
|
||||
#include "CompassRenderer.h"
|
||||
#include "NodeDB.h"
|
||||
#include "UIRenderer.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include <cmath>
|
||||
@@ -13,111 +9,103 @@ namespace graphics
|
||||
{
|
||||
namespace CompassRenderer
|
||||
{
|
||||
|
||||
// Point helper class for compass calculations
|
||||
struct Point {
|
||||
float x, y;
|
||||
Point(float x, float y) : x(x), y(y) {}
|
||||
|
||||
void rotate(float angle)
|
||||
{
|
||||
float cos_a = cos(angle);
|
||||
float sin_a = sin(angle);
|
||||
float new_x = x * cos_a - y * sin_a;
|
||||
float new_y = x * sin_a + y * cos_a;
|
||||
x = new_x;
|
||||
y = new_y;
|
||||
}
|
||||
|
||||
void scale(float factor)
|
||||
{
|
||||
x *= factor;
|
||||
y *= factor;
|
||||
}
|
||||
|
||||
void translate(float dx, float dy)
|
||||
{
|
||||
x += dx;
|
||||
y += dy;
|
||||
}
|
||||
};
|
||||
|
||||
void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading, int16_t radius)
|
||||
{
|
||||
// Show the compass heading (not implemented in original)
|
||||
// This could draw a "N" indicator or north arrow
|
||||
// For now, we'll draw a simple north indicator
|
||||
// const float radius = 17.0f;
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
radius += 4;
|
||||
}
|
||||
Point north(0, -radius);
|
||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING)
|
||||
north.rotate(-myHeading);
|
||||
north.translate(compassX, compassY);
|
||||
|
||||
const float northAngle = (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) ? -myHeading : 0.0f;
|
||||
const int16_t nX = compassX + static_cast<int16_t>((radius - 1) * sinf(northAngle));
|
||||
const int16_t nY = compassY - static_cast<int16_t>((radius - 1) * cosf(northAngle));
|
||||
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
#if !GRAPHICS_TFT_COLORING_ENABLED
|
||||
display->setColor(BLACK);
|
||||
const int16_t nLabelWidth = display->getStringWidth("N");
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6);
|
||||
display->fillRect(nX - 8, nY - 1, nLabelWidth + 3, FONT_HEIGHT_SMALL - 6);
|
||||
} else {
|
||||
display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6);
|
||||
display->fillRect(nX - 4, nY - 1, nLabelWidth + 2, FONT_HEIGHT_SMALL - 6);
|
||||
}
|
||||
display->setColor(WHITE);
|
||||
display->drawString(north.x, north.y - 3, "N");
|
||||
}
|
||||
|
||||
void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian)
|
||||
{
|
||||
Point tip(0.0f, -0.5f), tail(0.0f, 0.35f); // pointing up initially
|
||||
float arrowOffsetX = 0.14f, arrowOffsetY = 0.9f;
|
||||
Point leftArrow(tip.x - arrowOffsetX, tip.y + arrowOffsetY), rightArrow(tip.x + arrowOffsetX, tip.y + arrowOffsetY);
|
||||
|
||||
Point *arrowPoints[] = {&tip, &tail, &leftArrow, &rightArrow};
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
arrowPoints[i]->rotate(headingRadian);
|
||||
arrowPoints[i]->scale(compassDiam * 0.6);
|
||||
arrowPoints[i]->translate(compassX, compassY);
|
||||
}
|
||||
|
||||
#ifdef USE_EINK
|
||||
display->drawTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y);
|
||||
#else
|
||||
display->fillTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y);
|
||||
#endif
|
||||
display->drawTriangle(tip.x, tip.y, leftArrow.x, leftArrow.y, tail.x, tail.y);
|
||||
display->setColor(WHITE);
|
||||
display->drawString(nX, nY - 3, "N");
|
||||
}
|
||||
|
||||
void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing)
|
||||
{
|
||||
float radians = bearing * DEG_TO_RAD;
|
||||
const float radians = bearing * DEG_TO_RAD;
|
||||
const float sinA = sinf(radians);
|
||||
const float cosA = cosf(radians);
|
||||
const float tipHalf = size * 0.5f;
|
||||
const float lx = -(size / 6.0f);
|
||||
const float ly = size / 4.0f;
|
||||
const float rx = (size / 6.0f);
|
||||
const float ry = size / 4.0f;
|
||||
const float tx = 0.0f;
|
||||
const float ty = size / 4.5f;
|
||||
|
||||
Point tip(0, -size / 2);
|
||||
Point left(-size / 6, size / 4);
|
||||
Point right(size / 6, size / 4);
|
||||
Point tail(0, size / 4.5);
|
||||
const int16_t tipX = static_cast<int16_t>(x + (tipHalf * sinA));
|
||||
const int16_t tipY = static_cast<int16_t>(y - (tipHalf * cosA));
|
||||
const int16_t leftX = static_cast<int16_t>(x + (lx * cosA) - (ly * sinA));
|
||||
const int16_t leftY = static_cast<int16_t>(y + (lx * sinA) + (ly * cosA));
|
||||
const int16_t rightX = static_cast<int16_t>(x + (rx * cosA) - (ry * sinA));
|
||||
const int16_t rightY = static_cast<int16_t>(y + (rx * sinA) + (ry * cosA));
|
||||
const int16_t tailX = static_cast<int16_t>(x + (tx * cosA) - (ty * sinA));
|
||||
const int16_t tailY = static_cast<int16_t>(y + (tx * sinA) + (ty * cosA));
|
||||
|
||||
tip.rotate(radians);
|
||||
left.rotate(radians);
|
||||
right.rotate(radians);
|
||||
tail.rotate(radians);
|
||||
|
||||
tip.translate(x, y);
|
||||
left.translate(x, y);
|
||||
right.translate(x, y);
|
||||
tail.translate(x, y);
|
||||
|
||||
display->fillTriangle(tip.x, tip.y, left.x, left.y, tail.x, tail.y);
|
||||
display->fillTriangle(tip.x, tip.y, right.x, right.y, tail.x, tail.y);
|
||||
display->fillTriangle(tipX, tipY, leftX, leftY, tailX, tailY);
|
||||
display->fillTriangle(tipX, tipY, rightX, rightY, tailX, tailY);
|
||||
}
|
||||
|
||||
float estimatedHeading(double lat, double lon)
|
||||
void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian)
|
||||
{
|
||||
// Simple magnetic declination estimation
|
||||
// This is a very basic implementation - the original might be more sophisticated
|
||||
return 0.0f; // Return 0 for now, indicating no heading available
|
||||
const int16_t size = static_cast<int16_t>(compassDiam * 0.6f);
|
||||
drawArrowToNode(display, compassX, compassY, size, headingRadian * RAD_TO_DEG);
|
||||
}
|
||||
|
||||
bool getHeadingRadians(double lat, double lon, float &headingRadian)
|
||||
{
|
||||
headingRadian = 0.0f;
|
||||
|
||||
if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING)
|
||||
return true;
|
||||
|
||||
if (!screen)
|
||||
return false;
|
||||
|
||||
if (screen->hasHeading()) {
|
||||
headingRadian = screen->getHeading() * DEG_TO_RAD;
|
||||
return true;
|
||||
}
|
||||
|
||||
const float estimatedHeadingDeg = screen->estimatedHeading(lat, lon);
|
||||
if (!(estimatedHeadingDeg >= 0.0f))
|
||||
return false;
|
||||
|
||||
headingRadian = estimatedHeadingDeg * DEG_TO_RAD;
|
||||
return true;
|
||||
}
|
||||
|
||||
float adjustBearingForCompassMode(float bearingRadian, float headingRadian)
|
||||
{
|
||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING)
|
||||
return bearingRadian - headingRadian;
|
||||
|
||||
return bearingRadian;
|
||||
}
|
||||
|
||||
float radiansToDegrees360(float angleRadian)
|
||||
{
|
||||
constexpr float fullTurnDeg = 360.0f;
|
||||
float degrees = angleRadian * RAD_TO_DEG;
|
||||
if (degrees < 0.0f)
|
||||
degrees += fullTurnDeg;
|
||||
else if (degrees >= fullTurnDeg)
|
||||
degrees -= fullTurnDeg;
|
||||
return degrees;
|
||||
}
|
||||
|
||||
uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight)
|
||||
@@ -137,4 +125,4 @@ uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight)
|
||||
|
||||
} // namespace CompassRenderer
|
||||
} // namespace graphics
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -25,7 +25,9 @@ void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, u
|
||||
void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing);
|
||||
|
||||
// Navigation and location functions
|
||||
float estimatedHeading(double lat, double lon);
|
||||
bool getHeadingRadians(double lat, double lon, float &headingRadian);
|
||||
float adjustBearingForCompassMode(float bearingRadian, float headingRadian);
|
||||
float radiansToDegrees360(float angleRadian);
|
||||
uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight);
|
||||
|
||||
} // namespace CompassRenderer
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#include "gps/RTC.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/TFTColorRegions.h"
|
||||
#include "graphics/TFTPalette.h"
|
||||
#include "graphics/TimeFormatters.h"
|
||||
#include "graphics/images.h"
|
||||
#include "main.h"
|
||||
@@ -408,7 +410,16 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
||||
display->drawString(nameX, getTextPositions(display)[line++], device_role);
|
||||
|
||||
// === Third Row: Radio Preset ===
|
||||
auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset);
|
||||
// For custom modem settings show the actual parameters; for presets use the preset name.
|
||||
char modeStr[16];
|
||||
if (!config.lora.use_preset) {
|
||||
snprintf(modeStr, sizeof(modeStr), "BW%u-SF%u-CR%u", static_cast<unsigned>(config.lora.bandwidth),
|
||||
static_cast<unsigned>(config.lora.spread_factor), static_cast<unsigned>(config.lora.coding_rate));
|
||||
} else {
|
||||
strncpy(modeStr, DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, true),
|
||||
sizeof(modeStr) - 1);
|
||||
modeStr[sizeof(modeStr) - 1] = '\0';
|
||||
}
|
||||
|
||||
char regionradiopreset[25];
|
||||
const char *region = myRegion ? myRegion->name : NULL;
|
||||
@@ -416,7 +427,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
||||
if (currentResolution == ScreenResolution::UltraLow) {
|
||||
snprintf(regionradiopreset, sizeof(regionradiopreset), "%s", region);
|
||||
} else {
|
||||
snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode);
|
||||
snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, modeStr);
|
||||
}
|
||||
}
|
||||
textWidth = display->getStringWidth(regionradiopreset);
|
||||
@@ -460,9 +471,11 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
||||
int chUtil_y = getTextPositions(display)[line] + 3;
|
||||
|
||||
int chutil_bar_width = (currentResolution == ScreenResolution::High) ? 100 : 50;
|
||||
int chutil_bar_max_fill = chutil_bar_width - 2; // Account for border
|
||||
int chutil_bar_height = (currentResolution == ScreenResolution::High) ? 12 : 7;
|
||||
int extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 3;
|
||||
int chutil_percent = airTime->channelUtilizationPercent();
|
||||
const int raw_chutil_percent = chutil_percent;
|
||||
|
||||
int centerofscreen = SCREEN_WIDTH / 2;
|
||||
int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2;
|
||||
@@ -470,7 +483,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
||||
|
||||
display->drawString(starting_position, getTextPositions(display)[line], chUtil);
|
||||
|
||||
// Force 56% or higher to show a full 100% bar, text would still show related percent.
|
||||
// Force 61% or higher to show a full 100% bar, text would still show related percent.
|
||||
if (chutil_percent >= 61) {
|
||||
chutil_percent = 100;
|
||||
}
|
||||
@@ -483,9 +496,9 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
||||
float weight3 = 0.20; // Weight for 40–100%
|
||||
float totalWeight = weight1 + weight2 + weight3;
|
||||
|
||||
int seg1 = chutil_bar_width * (weight1 / totalWeight);
|
||||
int seg2 = chutil_bar_width * (weight2 / totalWeight);
|
||||
int seg3 = chutil_bar_width * (weight3 / totalWeight);
|
||||
int seg1 = chutil_bar_max_fill * (weight1 / totalWeight);
|
||||
int seg2 = chutil_bar_max_fill * (weight2 / totalWeight);
|
||||
int seg3 = chutil_bar_max_fill - seg1 - seg2; // Remainder absorbs rounding errors
|
||||
|
||||
int fillRight = 0;
|
||||
|
||||
@@ -502,7 +515,17 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
||||
|
||||
// Fill progress
|
||||
if (fillRight > 0) {
|
||||
display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height);
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
uint16_t UtilizationFillColor = TFTPalette::Good;
|
||||
if (raw_chutil_percent >= 60) {
|
||||
UtilizationFillColor = TFTPalette::Bad;
|
||||
} else if (raw_chutil_percent >= 35) {
|
||||
UtilizationFillColor = TFTPalette::Medium;
|
||||
}
|
||||
setAndRegisterTFTColorRole(TFTColorRole::UtilizationFill, UtilizationFillColor, TFTPalette::Black,
|
||||
starting_position + chUtil_x + 1, chUtil_y + 1, fillRight, chutil_bar_height - 2);
|
||||
#endif
|
||||
display->fillRect(starting_position + chUtil_x + 1, chUtil_y + 1, fillRight, chutil_bar_height - 2);
|
||||
}
|
||||
|
||||
display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line++],
|
||||
@@ -575,6 +598,17 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
|
||||
display->setColor(WHITE);
|
||||
display->drawRect(barX, barY, adjustedBarWidth, barHeight);
|
||||
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
uint16_t UtilizationFillColor = TFTPalette::Good;
|
||||
if (percent >= 80) {
|
||||
UtilizationFillColor = TFTPalette::Bad;
|
||||
} else if (percent >= 60) {
|
||||
UtilizationFillColor = TFTPalette::Medium;
|
||||
}
|
||||
setAndRegisterTFTColorRole(TFTColorRole::UtilizationFill, UtilizationFillColor, TFTPalette::Black, barX + 1, barY + 1,
|
||||
fillWidth - 1, barHeight - 2);
|
||||
#endif
|
||||
|
||||
display->fillRect(barX, barY, fillWidth, barHeight);
|
||||
display->setColor(WHITE);
|
||||
#endif
|
||||
|
||||
+181
-192
@@ -11,6 +11,7 @@
|
||||
#include "buzz.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/TFTColorRegions.h"
|
||||
#include "graphics/draw/MessageRenderer.h"
|
||||
#include "graphics/draw/UIRenderer.h"
|
||||
#include "input/RotaryEncoderInterruptImpl1.h"
|
||||
@@ -18,6 +19,7 @@
|
||||
#include "main.h"
|
||||
#include "mesh/Default.h"
|
||||
#include "mesh/MeshTypes.h"
|
||||
#include "mesh/RadioLibInterface.h"
|
||||
#include "modules/AdminModule.h"
|
||||
#include "modules/CannedMessageModule.h"
|
||||
#include "modules/ExternalNotificationModule.h"
|
||||
@@ -25,11 +27,10 @@
|
||||
#include "modules/TraceRouteModule.h"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
|
||||
extern uint16_t TFT_MESH;
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
|
||||
@@ -56,6 +57,70 @@ BannerOverlayOptions createStaticBannerOptions(const char *message, const MenuOp
|
||||
return bannerOptions;
|
||||
}
|
||||
|
||||
const StoredMessage *getNewestMessageForActiveThread()
|
||||
{
|
||||
const auto &messages = messageStore.getMessages();
|
||||
if (messages.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto mode = graphics::MessageRenderer::getThreadMode();
|
||||
const int channel = graphics::MessageRenderer::getThreadChannel();
|
||||
const uint32_t peer = graphics::MessageRenderer::getThreadPeer();
|
||||
const uint32_t localNode = nodeDB->getNodeNum();
|
||||
|
||||
if (mode == graphics::MessageRenderer::ThreadMode::ALL) {
|
||||
return &messages.back();
|
||||
}
|
||||
|
||||
for (auto it = messages.rbegin(); it != messages.rend(); ++it) {
|
||||
const StoredMessage &m = *it;
|
||||
|
||||
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
|
||||
if (m.type == MessageType::BROADCAST && static_cast<int>(m.channelIndex) == channel) {
|
||||
return &m;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
|
||||
if (m.type != MessageType::DM_TO_US) {
|
||||
continue;
|
||||
}
|
||||
const uint32_t other = (m.sender == localNode) ? m.dest : m.sender;
|
||||
if (other == peer) {
|
||||
return &m;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void launchReplyForMessage(const StoredMessage &message, bool freetext)
|
||||
{
|
||||
if (message.type == MessageType::BROADCAST || message.dest == NODENUM_BROADCAST) {
|
||||
if (freetext) {
|
||||
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, message.channelIndex);
|
||||
} else {
|
||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, message.channelIndex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t localNode = nodeDB->getNodeNum();
|
||||
const uint32_t peer = (message.sender == localNode) ? message.dest : message.sender;
|
||||
if (peer == 0 || peer == NODENUM_BROADCAST) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (freetext) {
|
||||
cannedMessageModule->LaunchFreetextWithDestination(peer);
|
||||
} else {
|
||||
cannedMessageModule->LaunchWithDestination(peer);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
menuHandler::screenMenus menuHandler::menuQueue = MenuNone;
|
||||
@@ -159,31 +224,22 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||
return;
|
||||
}
|
||||
|
||||
// Guard: without a reboot, reconfigure() applies the region directly.
|
||||
// Reject LORA_24 on sub-GHz-only hardware — getRadio() used to catch this post-reboot.
|
||||
// TODO: change this to either use the validateLoraConfig() logic or at least check the region for wideLora
|
||||
// rather than a hardcoded check for LORA_24.
|
||||
if (selectedRegion == meshtastic_Config_LoRaConfig_RegionCode_LORA_24 &&
|
||||
!(RadioLibInterface::instance && RadioLibInterface::instance->wideLora())) {
|
||||
LOG_WARN("Radio hardware does not support 2.4 GHz; ignoring region selection");
|
||||
return;
|
||||
}
|
||||
|
||||
config.lora.region = selectedRegion;
|
||||
auto changes = SEGMENT_CONFIG;
|
||||
|
||||
// FIXME: This should be a method consolidated with the same logic in the admin message as well
|
||||
// This is needed as we wait til picking the LoRa region to generate keys for the first time.
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI)
|
||||
if (!owner.is_licensed) {
|
||||
bool keygenSuccess = false;
|
||||
if (config.security.private_key.size == 32) {
|
||||
// public key is derived from private, so this will always have the same result.
|
||||
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
|
||||
keygenSuccess = true;
|
||||
}
|
||||
|
||||
} else {
|
||||
LOG_INFO("Generate new PKI keys");
|
||||
crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes);
|
||||
keygenSuccess = true;
|
||||
}
|
||||
if (keygenSuccess) {
|
||||
config.security.public_key.size = 32;
|
||||
config.security.private_key.size = 32;
|
||||
owner.public_key.size = 32;
|
||||
memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32);
|
||||
}
|
||||
if (crypto) {
|
||||
crypto->ensurePkiKeys(config.security, owner);
|
||||
}
|
||||
#endif
|
||||
config.lora.tx_enabled = true;
|
||||
@@ -199,7 +255,6 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||
}
|
||||
|
||||
service->reloadConfig(changes);
|
||||
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||||
});
|
||||
|
||||
bannerOptions.durationMs = duration;
|
||||
@@ -265,13 +320,24 @@ void menuHandler::FrequencySlotPicker()
|
||||
optionsEnumArray[options++] = 0;
|
||||
|
||||
// Calculate number of channels (copied from RadioInterface::applyModemConfig())
|
||||
|
||||
meshtastic_Config_LoRaConfig &loraConfig = config.lora;
|
||||
double bw = loraConfig.use_preset ? modemPresetToBwKHz(loraConfig.modem_preset, myRegion->wideLora)
|
||||
: bwCodeToKHz(loraConfig.bandwidth);
|
||||
|
||||
uint32_t numChannels = 0;
|
||||
if (myRegion) {
|
||||
numChannels = (uint32_t)floor((myRegion->freqEnd - myRegion->freqStart) / (myRegion->spacing + (bw / 1000.0)));
|
||||
// Match RadioInterface::applyModemConfig(): include padding, add spacing in numerator, and use round()
|
||||
const double spacing = myRegion->profile->spacing;
|
||||
const double padding = myRegion->profile->padding;
|
||||
const double channelBandwidthMHz = bw / 1000.0;
|
||||
const double numerator = (myRegion->freqEnd - myRegion->freqStart) + spacing;
|
||||
const double denominator = spacing + (padding * 2) + channelBandwidthMHz;
|
||||
if (denominator > 0.0) {
|
||||
numChannels = static_cast<uint32_t>(round(numerator / denominator));
|
||||
} else {
|
||||
LOG_WARN("Invalid region configuration: non-positive channel spacing/width");
|
||||
}
|
||||
} else {
|
||||
LOG_WARN("Region not set, cannot calculate number of channels");
|
||||
return;
|
||||
@@ -307,7 +373,6 @@ void menuHandler::FrequencySlotPicker()
|
||||
|
||||
config.lora.channel_num = selected;
|
||||
service->reloadConfig(SEGMENT_CONFIG);
|
||||
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||||
};
|
||||
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
@@ -346,7 +411,6 @@ void menuHandler::radioPresetPicker()
|
||||
config.lora.channel_num = 0; // Reset to default channel for the preset
|
||||
config.lora.override_frequency = 0; // Clear any custom frequency
|
||||
service->reloadConfig(SEGMENT_CONFIG);
|
||||
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||||
});
|
||||
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
@@ -594,9 +658,12 @@ void menuHandler::messageResponseMenu()
|
||||
|
||||
#ifdef HAS_I2S
|
||||
} else if (selected == Aloud) {
|
||||
const meshtastic_MeshPacket &mp = devicestate.rx_text_message;
|
||||
const char *msg = reinterpret_cast<const char *>(mp.decoded.payload.bytes);
|
||||
audioThread->readAloud(msg);
|
||||
if (const StoredMessage *latest = getNewestMessageForActiveThread()) {
|
||||
const char *msg = MessageStore::getText(*latest);
|
||||
if (msg && msg[0]) {
|
||||
audioThread->readAloud(msg);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
};
|
||||
@@ -656,20 +723,12 @@ void menuHandler::replyMenu()
|
||||
|
||||
// Preset reply
|
||||
if (selected == ReplyPreset) {
|
||||
|
||||
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
|
||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, ch);
|
||||
|
||||
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
|
||||
cannedMessageModule->LaunchWithDestination(peer);
|
||||
|
||||
} else {
|
||||
// Fallback for last received message
|
||||
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
|
||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
|
||||
} else {
|
||||
cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from);
|
||||
}
|
||||
} else if (const StoredMessage *latest = getNewestMessageForActiveThread()) {
|
||||
launchReplyForMessage(*latest, false);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -677,20 +736,12 @@ void menuHandler::replyMenu()
|
||||
|
||||
// Freetext reply
|
||||
if (selected == ReplyFreetext) {
|
||||
|
||||
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
|
||||
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, ch);
|
||||
|
||||
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
|
||||
cannedMessageModule->LaunchFreetextWithDestination(peer);
|
||||
|
||||
} else {
|
||||
// Fallback for last received message
|
||||
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
|
||||
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
|
||||
} else {
|
||||
cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from);
|
||||
}
|
||||
} else if (const StoredMessage *latest = getNewestMessageForActiveThread()) {
|
||||
launchReplyForMessage(*latest, true);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -845,10 +896,10 @@ void menuHandler::messageViewModeMenu()
|
||||
// Encode peers
|
||||
for (size_t i = 0; i < uniquePeers.size(); ++i) {
|
||||
uint32_t peer = uniquePeers[i];
|
||||
auto node = nodeDB->getMeshNode(peer);
|
||||
const auto *node = nodeDB->getMeshNode(peer);
|
||||
std::string name;
|
||||
if (node && node->has_user)
|
||||
name = sanitizeString(node->user.long_name).substr(0, 15);
|
||||
if (nodeInfoLiteHasUser(node))
|
||||
name = sanitizeString(node->long_name).substr(0, 15);
|
||||
else {
|
||||
char buf[20];
|
||||
snprintf(buf, sizeof(buf), "Node %08X", peer);
|
||||
@@ -1355,14 +1406,14 @@ void menuHandler::manageNodeMenu()
|
||||
static int optionsEnumArray[enumEnd] = {Back};
|
||||
int options = 1;
|
||||
|
||||
if (node->is_favorite) {
|
||||
if (nodeInfoLiteIsFavorite(node)) {
|
||||
optionsArray[options] = "Unfavorite";
|
||||
} else {
|
||||
optionsArray[options] = "Favorite";
|
||||
}
|
||||
optionsEnumArray[options++] = Favorite;
|
||||
|
||||
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
||||
bool isMuted = nodeInfoLiteIsMuted(node);
|
||||
if (isMuted) {
|
||||
optionsArray[options] = "Unmute Notifications";
|
||||
} else {
|
||||
@@ -1376,7 +1427,7 @@ void menuHandler::manageNodeMenu()
|
||||
optionsArray[options] = "Key Verification";
|
||||
optionsEnumArray[options++] = KeyVerification;
|
||||
|
||||
if (node->is_ignored) {
|
||||
if (nodeInfoLiteIsIgnored(node)) {
|
||||
optionsArray[options] = "Unignore Node";
|
||||
} else {
|
||||
optionsArray[options] = "Ignore Node";
|
||||
@@ -1386,8 +1437,8 @@ void menuHandler::manageNodeMenu()
|
||||
BannerOverlayOptions bannerOptions;
|
||||
|
||||
std::string title = "";
|
||||
if (node->has_user && node->user.long_name && node->user.long_name[0]) {
|
||||
title += sanitizeString(node->user.long_name).substr(0, 15);
|
||||
if (nodeInfoLiteHasUser(node) && node->long_name[0]) {
|
||||
title += sanitizeString(node->long_name).substr(0, 15);
|
||||
} else {
|
||||
char buf[20];
|
||||
snprintf(buf, sizeof(buf), "%08X", (unsigned int)node->num);
|
||||
@@ -1409,7 +1460,7 @@ void menuHandler::manageNodeMenu()
|
||||
if (!n) {
|
||||
return;
|
||||
}
|
||||
if (n->is_favorite) {
|
||||
if (nodeInfoLiteIsFavorite(n)) {
|
||||
LOG_INFO("Removing node %08X from favorites", menuHandler::pickedNodeNum);
|
||||
nodeDB->set_favorite(false, menuHandler::pickedNodeNum);
|
||||
} else {
|
||||
@@ -1426,13 +1477,9 @@ void menuHandler::manageNodeMenu()
|
||||
return;
|
||||
}
|
||||
|
||||
if (n->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) {
|
||||
n->bitfield &= ~NODEINFO_BITFIELD_IS_MUTED_MASK;
|
||||
LOG_INFO("Unmuted node %08X", menuHandler::pickedNodeNum);
|
||||
} else {
|
||||
n->bitfield |= NODEINFO_BITFIELD_IS_MUTED_MASK;
|
||||
LOG_INFO("Muted node %08X", menuHandler::pickedNodeNum);
|
||||
}
|
||||
const bool wasMuted = nodeInfoLiteIsMuted(n);
|
||||
nodeInfoLiteSetBit(n, NODEINFO_BITFIELD_IS_MUTED_MASK, !wasMuted);
|
||||
LOG_INFO(wasMuted ? "Unmuted node %08X" : "Muted node %08X", menuHandler::pickedNodeNum);
|
||||
nodeDB->notifyObservers(true);
|
||||
nodeDB->saveToDisk();
|
||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
||||
@@ -1461,11 +1508,11 @@ void menuHandler::manageNodeMenu()
|
||||
return;
|
||||
}
|
||||
|
||||
if (n->is_ignored) {
|
||||
n->is_ignored = false;
|
||||
if (nodeInfoLiteIsIgnored(n)) {
|
||||
nodeInfoLiteSetBit(n, NODEINFO_BITFIELD_IS_IGNORED_MASK, false);
|
||||
LOG_INFO("Unignoring node %08X", menuHandler::pickedNodeNum);
|
||||
} else {
|
||||
n->is_ignored = true;
|
||||
nodeInfoLiteSetBit(n, NODEINFO_BITFIELD_IS_IGNORED_MASK, true);
|
||||
LOG_INFO("Ignoring node %08X", menuHandler::pickedNodeNum);
|
||||
}
|
||||
nodeDB->notifyObservers(true);
|
||||
@@ -2027,109 +2074,6 @@ void menuHandler::switchToMUIMenu()
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::TFTColorPickerMenu(OLEDDisplay *display)
|
||||
{
|
||||
static const ScreenColorOption colorOptions[] = {
|
||||
{"Back", OptionsAction::Back},
|
||||
{"Default", OptionsAction::Select, ScreenColor(0, 0, 0, true)},
|
||||
{"Meshtastic Green", OptionsAction::Select, ScreenColor(0x67, 0xEA, 0x94)},
|
||||
{"Yellow", OptionsAction::Select, ScreenColor(255, 255, 128)},
|
||||
{"Red", OptionsAction::Select, ScreenColor(255, 64, 64)},
|
||||
{"Orange", OptionsAction::Select, ScreenColor(255, 160, 20)},
|
||||
{"Purple", OptionsAction::Select, ScreenColor(204, 153, 255)},
|
||||
{"Blue", OptionsAction::Select, ScreenColor(0, 0, 255)},
|
||||
{"Teal", OptionsAction::Select, ScreenColor(16, 102, 102)},
|
||||
{"Cyan", OptionsAction::Select, ScreenColor(0, 255, 255)},
|
||||
{"Ice", OptionsAction::Select, ScreenColor(173, 216, 230)},
|
||||
{"Pink", OptionsAction::Select, ScreenColor(255, 105, 180)},
|
||||
{"White", OptionsAction::Select, ScreenColor(255, 255, 255)},
|
||||
{"Gray", OptionsAction::Select, ScreenColor(128, 128, 128)},
|
||||
};
|
||||
|
||||
constexpr size_t colorCount = sizeof(colorOptions) / sizeof(colorOptions[0]);
|
||||
static std::array<const char *, colorCount> colorLabels{};
|
||||
|
||||
auto bannerOptions = createStaticBannerOptions(
|
||||
"Select Screen Color", colorOptions, colorLabels, [display](const ScreenColorOption &option, int) -> void {
|
||||
if (option.action == OptionsAction::Back) {
|
||||
menuQueue = SystemBaseMenu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!option.hasValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || \
|
||||
HAS_TFT || defined(HACKADAY_COMMUNICATOR)
|
||||
const ScreenColor &color = option.value;
|
||||
if (color.useVariant) {
|
||||
LOG_INFO("Setting color to system default or defined variant");
|
||||
} else {
|
||||
LOG_INFO("Setting color to %s", option.label);
|
||||
}
|
||||
|
||||
uint8_t r = color.r;
|
||||
uint8_t g = color.g;
|
||||
uint8_t b = color.b;
|
||||
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
|
||||
display->setColor(WHITE);
|
||||
|
||||
if (color.useVariant || (r == 0 && g == 0 && b == 0)) {
|
||||
#ifdef TFT_MESH_OVERRIDE
|
||||
TFT_MESH = TFT_MESH_OVERRIDE;
|
||||
#else
|
||||
TFT_MESH = COLOR565(255, 255, 128);
|
||||
#endif
|
||||
} else {
|
||||
TFT_MESH = COLOR565(r, g, b);
|
||||
}
|
||||
|
||||
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190)
|
||||
static_cast<ST7789Spi *>(screen->getDisplayDevice())->setRGB(TFT_MESH);
|
||||
#endif
|
||||
|
||||
screen->setFrames(graphics::Screen::FOCUS_SYSTEM);
|
||||
if (color.useVariant || (r == 0 && g == 0 && b == 0)) {
|
||||
uiconfig.screen_rgb_color = 0;
|
||||
} else {
|
||||
uiconfig.screen_rgb_color =
|
||||
(static_cast<uint32_t>(r) << 16) | (static_cast<uint32_t>(g) << 8) | static_cast<uint32_t>(b);
|
||||
}
|
||||
LOG_INFO("Storing Value of %d to uiconfig.screen_rgb_color", uiconfig.screen_rgb_color);
|
||||
saveUIConfig();
|
||||
#endif
|
||||
});
|
||||
|
||||
int initialSelection = 0;
|
||||
if (uiconfig.screen_rgb_color == 0) {
|
||||
initialSelection = 1;
|
||||
} else {
|
||||
uint32_t currentColor = uiconfig.screen_rgb_color;
|
||||
for (size_t i = 0; i < colorCount; ++i) {
|
||||
if (!colorOptions[i].hasValue) {
|
||||
continue;
|
||||
}
|
||||
const ScreenColor &color = colorOptions[i].value;
|
||||
if (color.useVariant) {
|
||||
continue;
|
||||
}
|
||||
uint32_t encoded =
|
||||
(static_cast<uint32_t>(color.r) << 16) | (static_cast<uint32_t>(color.g) << 8) | static_cast<uint32_t>(color.b);
|
||||
if (encoded == currentColor) {
|
||||
initialSelection = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
bannerOptions.InitialSelected = initialSelection;
|
||||
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::rebootMenu()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "Confirm"};
|
||||
@@ -2182,9 +2126,9 @@ void menuHandler::removeFavoriteMenu()
|
||||
static const char *optionsArray[] = {"Back", "Yes"};
|
||||
BannerOverlayOptions bannerOptions;
|
||||
std::string message = "Unfavorite This Node?\n";
|
||||
auto node = nodeDB->getMeshNode(graphics::UIRenderer::currentFavoriteNodeNum);
|
||||
if (node && node->has_user) {
|
||||
message += sanitizeString(node->user.long_name).substr(0, 15);
|
||||
const auto *node = nodeDB->getMeshNode(graphics::UIRenderer::currentFavoriteNodeNum);
|
||||
if (nodeInfoLiteHasUser(node)) {
|
||||
message += sanitizeString(node->long_name).substr(0, 15);
|
||||
}
|
||||
bannerOptions.message = message.c_str();
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
@@ -2317,9 +2261,9 @@ void menuHandler::screenOptionsMenu()
|
||||
bool hasSupportBrightness = false;
|
||||
#endif
|
||||
|
||||
enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits, MessageBubbles };
|
||||
static const char *optionsArray[6] = {"Back"};
|
||||
static int optionsEnumArray[6] = {Back};
|
||||
enum optionsNumbers { Back, Brightness, FrameToggles, DisplayUnits, MessageBubbles, Theme };
|
||||
static const char *optionsArray[7] = {"Back"};
|
||||
static int optionsEnumArray[7] = {Back};
|
||||
int options = 1;
|
||||
|
||||
// Only show brightness for B&W displays
|
||||
@@ -2328,13 +2272,6 @@ void menuHandler::screenOptionsMenu()
|
||||
optionsEnumArray[options++] = Brightness;
|
||||
}
|
||||
|
||||
// Only show screen color for TFT displays
|
||||
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || \
|
||||
HAS_TFT || defined(HACKADAY_COMMUNICATOR)
|
||||
optionsArray[options] = "Screen Color";
|
||||
optionsEnumArray[options++] = ScreenColor;
|
||||
#endif
|
||||
|
||||
optionsArray[options] = "Frame Visibility";
|
||||
optionsEnumArray[options++] = FrameToggles;
|
||||
|
||||
@@ -2344,6 +2281,11 @@ void menuHandler::screenOptionsMenu()
|
||||
optionsArray[options] = "Message Bubbles";
|
||||
optionsEnumArray[options++] = MessageBubbles;
|
||||
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
optionsArray[options] = "Theme";
|
||||
optionsEnumArray[options++] = Theme;
|
||||
#endif
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Display Options";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
@@ -2353,9 +2295,6 @@ void menuHandler::screenOptionsMenu()
|
||||
if (selected == Brightness) {
|
||||
menuHandler::menuQueue = menuHandler::BrightnessPicker;
|
||||
screen->runNow();
|
||||
} else if (selected == ScreenColor) {
|
||||
menuHandler::menuQueue = menuHandler::TftColorMenuPicker;
|
||||
screen->runNow();
|
||||
} else if (selected == FrameToggles) {
|
||||
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||
screen->runNow();
|
||||
@@ -2365,6 +2304,9 @@ void menuHandler::screenOptionsMenu()
|
||||
} else if (selected == MessageBubbles) {
|
||||
menuHandler::menuQueue = menuHandler::MessageBubblesMenu;
|
||||
screen->runNow();
|
||||
} else if (selected == Theme) {
|
||||
menuHandler::menuQueue = menuHandler::ThemeMenu;
|
||||
screen->runNow();
|
||||
} else {
|
||||
menuQueue = SystemBaseMenu;
|
||||
screen->runNow();
|
||||
@@ -2648,6 +2590,53 @@ void menuHandler::messageBubblesMenu()
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::themeMenu()
|
||||
{
|
||||
// Build menu dynamically from the theme table.
|
||||
// Only visible themes appear!
|
||||
// Slot budget: 1 for "Back" + up to kMaxThemesInMenu visible themes.
|
||||
// Bump kMaxThemesInMenu if you add more themes than will fit here.
|
||||
constexpr size_t kMaxThemesInMenu = 15;
|
||||
const size_t visibleCount = getVisibleThemeCount();
|
||||
static const char *optionsArray[kMaxThemesInMenu + 1] = {"Back"};
|
||||
const size_t shownCount = (visibleCount < kMaxThemesInMenu) ? visibleCount : kMaxThemesInMenu;
|
||||
const int options = static_cast<int>(shownCount) + 1; // +1 for Back
|
||||
|
||||
for (size_t i = 0; i < shownCount; i++) {
|
||||
optionsArray[i + 1] = getVisibleThemeByIndex(i).name;
|
||||
}
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Theme";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = options;
|
||||
|
||||
// Highlight the currently active theme (visible index + 1 for the Back
|
||||
// offset). If the active theme is hidden, leave selection on "Back".
|
||||
const size_t activeVisible = getActiveVisibleThemeIndex();
|
||||
bannerOptions.InitialSelected = (activeVisible == SIZE_MAX) ? 0 : static_cast<int>(activeVisible) + 1;
|
||||
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == 0) {
|
||||
// Back
|
||||
menuHandler::menuQueue = menuHandler::ScreenOptionsMenu;
|
||||
screen->runNow();
|
||||
} else {
|
||||
// Selection is an index into the VISIBLE themes (1-based, slot 0 is Back).
|
||||
const size_t visibleIdx = static_cast<size_t>(selected - 1);
|
||||
if (visibleIdx < getVisibleThemeCount()) {
|
||||
// Persist the theme's uniqueIdentifier so boot-time
|
||||
// resolveThemeIndex() can restore this theme on next startup.
|
||||
uiconfig.screen_rgb_color = COLOR565(255, 255, (getVisibleThemeByIndex(visibleIdx).uniqueIdentifier & 0x1F) << 3);
|
||||
loadThemeDefaults();
|
||||
saveUIConfig();
|
||||
screen->runNow();
|
||||
}
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
||||
{
|
||||
if (menuQueue != MenuNone)
|
||||
@@ -2723,9 +2712,6 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
||||
case MuiPicker:
|
||||
switchToMUIMenu();
|
||||
break;
|
||||
case TftColorMenuPicker:
|
||||
TFTColorPickerMenu(display);
|
||||
break;
|
||||
case BrightnessPicker:
|
||||
BrightnessPickerMenu();
|
||||
break;
|
||||
@@ -2798,6 +2784,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
||||
case MessageBubblesMenu:
|
||||
messageBubblesMenu();
|
||||
break;
|
||||
case ThemeMenu:
|
||||
themeMenu();
|
||||
break;
|
||||
}
|
||||
menuQueue = MenuNone;
|
||||
}
|
||||
@@ -2809,4 +2798,4 @@ void menuHandler::saveUIConfig()
|
||||
|
||||
} // namespace graphics
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -30,7 +30,6 @@ class menuHandler
|
||||
ResetNodeDbMenu,
|
||||
BuzzerModeMenuPicker,
|
||||
MuiPicker,
|
||||
TftColorMenuPicker,
|
||||
BrightnessPicker,
|
||||
RebootMenu,
|
||||
ShutdownMenu,
|
||||
@@ -55,7 +54,8 @@ class menuHandler
|
||||
NodeNameLengthMenu,
|
||||
FrameToggles,
|
||||
DisplayUnits,
|
||||
MessageBubblesMenu
|
||||
MessageBubblesMenu,
|
||||
ThemeMenu
|
||||
};
|
||||
static screenMenus menuQueue;
|
||||
static uint32_t pickedNodeNum; // node selected by NodePicker for ManageNodeMenu
|
||||
@@ -89,7 +89,6 @@ class menuHandler
|
||||
static void GPSPositionBroadcastMenu();
|
||||
static void BuzzerModeMenu();
|
||||
static void switchToMUIMenu();
|
||||
static void TFTColorPickerMenu(OLEDDisplay *display);
|
||||
static void nodeListMenu();
|
||||
static void resetNodeDBMenu();
|
||||
static void BrightnessPickerMenu();
|
||||
@@ -110,6 +109,7 @@ class menuHandler
|
||||
static void frameTogglesMenu();
|
||||
static void displayUnitsMenu();
|
||||
static void messageBubblesMenu();
|
||||
static void themeMenu();
|
||||
static void textMessageMenu();
|
||||
|
||||
private:
|
||||
@@ -136,23 +136,10 @@ template <typename T> struct MenuOption {
|
||||
MenuOption(const char *labelIn, OptionsAction actionIn) : label(labelIn), action(actionIn), hasValue(false), value() {}
|
||||
};
|
||||
|
||||
struct ScreenColor {
|
||||
uint8_t r;
|
||||
uint8_t g;
|
||||
uint8_t b;
|
||||
bool useVariant;
|
||||
|
||||
explicit ScreenColor(uint8_t rIn = 0, uint8_t gIn = 0, uint8_t bIn = 0, bool variantIn = false)
|
||||
: r(rIn), g(gIn), b(bIn), useVariant(variantIn)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
using RadioPresetOption = MenuOption<meshtastic_Config_LoRaConfig_ModemPreset>;
|
||||
using LoraRegionOption = MenuOption<meshtastic_Config_LoRaConfig_RegionCode>;
|
||||
using TimezoneOption = MenuOption<const char *>;
|
||||
using CompassOption = MenuOption<meshtastic_CompassMode>;
|
||||
using ScreenColorOption = MenuOption<ScreenColor>;
|
||||
using GPSToggleOption = MenuOption<meshtastic_Config_PositionConfig_GpsMode>;
|
||||
using GPSFormatOption = MenuOption<meshtastic_DeviceUIConfig_GpsCoordinateFormat>;
|
||||
using NodeNameOption = MenuOption<bool>;
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/TFTColorRegions.h"
|
||||
#include "graphics/TFTPalette.h"
|
||||
#include "graphics/TimeFormatters.h"
|
||||
#include "graphics/emotes.h"
|
||||
#include "main.h"
|
||||
@@ -254,6 +256,76 @@ struct MessageBlock {
|
||||
bool mine;
|
||||
};
|
||||
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
static void setDarkModeBubbleRoleColors(uint32_t themeId, bool mine)
|
||||
{
|
||||
uint16_t bubbleOnColor;
|
||||
uint16_t bubbleOffColor;
|
||||
|
||||
if (themeId == ThemeID::Blue) {
|
||||
bubbleOnColor = mine ? TFTPalette::Navy : TFTPalette::White;
|
||||
bubbleOffColor = mine ? TFTPalette::SkyBlue : TFTPalette::DeepBlue;
|
||||
} else {
|
||||
bubbleOnColor = mine ? TFTPalette::Black : getThemeBodyFg();
|
||||
bubbleOffColor = mine ? TFTPalette::SkyBlue : TFTPalette::DarkGray;
|
||||
}
|
||||
|
||||
setTFTColorRole(TFTColorRole::ActionMenuBody, bubbleOnColor, bubbleOffColor);
|
||||
}
|
||||
|
||||
static void registerRoundedBubbleFillRegion(int x, int y, int w, int h, int radius)
|
||||
{
|
||||
if (w <= 0 || h <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (radius <= 0 || w < 3 || h < 3) {
|
||||
registerTFTColorRegion(TFTColorRole::ActionMenuBody, x, y, w, h);
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep region count low so we don't churn MAX_TFT_COLOR_REGIONS while
|
||||
// scrolling long message lists (which can flatten older bubble corners).
|
||||
int capRows = 0;
|
||||
if (radius >= 4 && h >= 5) {
|
||||
capRows = 2; // 5 regions total (2 top caps + middle + 2 bottom caps)
|
||||
} else if (radius >= 2 && h >= 3) {
|
||||
capRows = 1; // 3 regions total
|
||||
}
|
||||
if (capRows <= 0) {
|
||||
registerTFTColorRegion(TFTColorRole::ActionMenuBody, x, y, w, h);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int row = 0; row < capRows; ++row) {
|
||||
int inset = 0;
|
||||
if (radius >= 4) {
|
||||
inset = (row == 0) ? 2 : 1;
|
||||
} else if (radius >= 2) {
|
||||
inset = 1;
|
||||
}
|
||||
const int stripW = w - (inset * 2);
|
||||
if (stripW <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const int topY = y + row;
|
||||
registerTFTColorRegion(TFTColorRole::ActionMenuBody, x + inset, topY, stripW, 1);
|
||||
|
||||
const int bottomY = y + h - 1 - row;
|
||||
if (bottomY != topY) {
|
||||
registerTFTColorRegion(TFTColorRole::ActionMenuBody, x + inset, bottomY, stripW, 1);
|
||||
}
|
||||
}
|
||||
|
||||
const int middleY = y + capRows;
|
||||
const int middleH = h - (capRows * 2);
|
||||
if (middleH > 0) {
|
||||
registerTFTColorRegion(TFTColorRole::ActionMenuBody, x, middleY, w, middleH);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
static int getDrawnLinePixelBottom(int lineTopY, const std::string &line, bool isHeaderLine)
|
||||
{
|
||||
if (isHeaderLine) {
|
||||
@@ -390,8 +462,8 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
}
|
||||
case ThreadMode::DIRECT: {
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(currentPeer);
|
||||
if (node && node->has_user && node->user.short_name[0]) {
|
||||
snprintf(titleStr, sizeof(titleStr), "@%s", node->user.short_name);
|
||||
if (nodeInfoLiteHasUser(node) && node->short_name[0]) {
|
||||
snprintf(titleStr, sizeof(titleStr), "@%s", node->short_name);
|
||||
} else {
|
||||
snprintf(titleStr, sizeof(titleStr), "@%08x", currentPeer);
|
||||
}
|
||||
@@ -513,11 +585,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
meshtastic_NodeInfoLite *node_recipient = nodeDB->getMeshNode(m.dest);
|
||||
|
||||
char senderName[64] = "";
|
||||
if (node && node->has_user) {
|
||||
if (node->user.long_name[0]) {
|
||||
strncpy(senderName, node->user.long_name, sizeof(senderName) - 1);
|
||||
} else if (node->user.short_name[0]) {
|
||||
strncpy(senderName, node->user.short_name, sizeof(senderName) - 1);
|
||||
if (nodeInfoLiteHasUser(node)) {
|
||||
if (node->long_name[0]) {
|
||||
strncpy(senderName, node->long_name, sizeof(senderName) - 1);
|
||||
} else if (node->short_name[0]) {
|
||||
strncpy(senderName, node->short_name, sizeof(senderName) - 1);
|
||||
}
|
||||
senderName[sizeof(senderName) - 1] = '\0';
|
||||
}
|
||||
@@ -527,18 +599,17 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
|
||||
// If this is *our own* message, override senderName to who the recipient was
|
||||
bool mine = (m.sender == nodeDB->getNodeNum());
|
||||
if (mine && node_recipient && node_recipient->has_user) {
|
||||
if (node_recipient->user.long_name[0]) {
|
||||
strncpy(senderName, node_recipient->user.long_name, sizeof(senderName) - 1);
|
||||
if (mine && nodeInfoLiteHasUser(node_recipient)) {
|
||||
if (node_recipient->long_name[0]) {
|
||||
strncpy(senderName, node_recipient->long_name, sizeof(senderName) - 1);
|
||||
senderName[sizeof(senderName) - 1] = '\0';
|
||||
} else if (node_recipient->user.short_name[0]) {
|
||||
strncpy(senderName, node_recipient->user.short_name, sizeof(senderName) - 1);
|
||||
} else if (node_recipient->short_name[0]) {
|
||||
strncpy(senderName, node_recipient->short_name, sizeof(senderName) - 1);
|
||||
senderName[sizeof(senderName) - 1] = '\0';
|
||||
}
|
||||
}
|
||||
// If recipient info is missing/empty, prefer a recipient identifier for outbound messages.
|
||||
if (mine && (!node_recipient || !node_recipient->has_user ||
|
||||
(!node_recipient->user.long_name[0] && !node_recipient->user.short_name[0]))) {
|
||||
if (mine && (!nodeInfoLiteHasUser(node_recipient) || (!node_recipient->long_name[0] && !node_recipient->short_name[0]))) {
|
||||
snprintf(senderName, sizeof(senderName), "(%08x)", m.dest);
|
||||
}
|
||||
|
||||
@@ -648,6 +719,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
const int contentBottom = scrollBottom; // already excludes nav line
|
||||
const int rightEdge = SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN;
|
||||
const int bubbleGapY = std::max(1, MESSAGE_BLOCK_GAP / 2);
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
const uint32_t themeId = getActiveTheme().id;
|
||||
// Blue is a dark variant but uses full frame inversion, Keep it on the same filled bubble style as Default Dark.
|
||||
const bool useDarkModeBubbleFill = showBubbles && (!isThemeFullFrameInvert() || themeId == ThemeID::Blue);
|
||||
#endif
|
||||
|
||||
std::vector<int> lineTop;
|
||||
lineTop.resize(cachedLines.size());
|
||||
@@ -686,6 +762,17 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
int visualBottom = getDrawnLinePixelBottom(lineTop[b.end], cachedLines[b.end], isHeader[b.end]);
|
||||
int bottomY = visualBottom + BUBBLE_PAD_Y;
|
||||
|
||||
// On high-res screens, keep a 1px gap under the header
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
const int minTopY = contentTop + 1;
|
||||
if (topY < minTopY) {
|
||||
// Preserve bubble height when we push it down from the header.
|
||||
const int shift = minTopY - topY;
|
||||
topY = minTopY;
|
||||
bottomY += shift;
|
||||
}
|
||||
}
|
||||
|
||||
if (bi + 1 < blocks.size()) {
|
||||
int nextHeaderIndex = (int)blocks[bi + 1].start;
|
||||
int nextTop = lineTop[nextHeaderIndex];
|
||||
@@ -735,24 +822,56 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
const int by = topY;
|
||||
const int bw = bubbleW;
|
||||
const int bh = bubbleH;
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
const bool drawBubbleOutline = !useDarkModeBubbleFill;
|
||||
#else
|
||||
const bool drawBubbleOutline = true;
|
||||
#endif
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
if (useDarkModeBubbleFill) {
|
||||
setDarkModeBubbleRoleColors(themeId, b.mine);
|
||||
registerRoundedBubbleFillRegion(bx, by, bw, bh, r);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Draw the 4 corner arcs using drawCircleQuads
|
||||
display->drawCircleQuads(bx + r, by + r, r, 0x2); // Top-left
|
||||
display->drawCircleQuads(bx + bw - r - 1, by + r, r, 0x1); // Top-right
|
||||
display->drawCircleQuads(bx + r, by + bh - r - 1, r, 0x4); // Bottom-left
|
||||
display->drawCircleQuads(bx + bw - r - 1, by + bh - r - 1, r, 0x8); // Bottom-right
|
||||
if (drawBubbleOutline) {
|
||||
// Draw the 4 corner arcs using drawCircleQuads
|
||||
display->drawCircleQuads(bx + r, by + r, r, 0x2); // Top-left
|
||||
display->drawCircleQuads(bx + bw - r - 1, by + r, r, 0x1); // Top-right
|
||||
display->drawCircleQuads(bx + r, by + bh - r - 1, r, 0x4); // Bottom-left
|
||||
display->drawCircleQuads(bx + bw - r - 1, by + bh - r - 1, r, 0x8); // Bottom-right
|
||||
|
||||
// Draw the 4 edges between corners
|
||||
display->drawHorizontalLine(bx + r, by, bw - 2 * r); // Top edge
|
||||
display->drawHorizontalLine(bx + r, by + bh - 1, bw - 2 * r); // Bottom edge
|
||||
display->drawVerticalLine(bx, by + r, bh - 2 * r); // Left edge
|
||||
display->drawVerticalLine(bx + bw - 1, by + r, bh - 2 * r); // Right edge
|
||||
// Draw the 4 edges between corners
|
||||
display->drawHorizontalLine(bx + r, by, bw - 2 * r); // Top edge
|
||||
display->drawHorizontalLine(bx + r, by + bh - 1, bw - 2 * r); // Bottom edge
|
||||
display->drawVerticalLine(bx, by + r, bh - 2 * r); // Left edge
|
||||
display->drawVerticalLine(bx + bw - 1, by + r, bh - 2 * r); // Right edge
|
||||
}
|
||||
} else if (bubbleW > 1 && bubbleH > 1) {
|
||||
// Fallback to simple rectangle for very small bubbles
|
||||
display->drawRect(bubbleX, topY, bubbleW, bubbleH);
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
const bool drawBubbleOutline = !useDarkModeBubbleFill;
|
||||
#else
|
||||
const bool drawBubbleOutline = true;
|
||||
#endif
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
if (useDarkModeBubbleFill) {
|
||||
setDarkModeBubbleRoleColors(themeId, b.mine);
|
||||
registerTFTColorRegion(TFTColorRole::ActionMenuBody, bubbleX, topY, bubbleW, bubbleH);
|
||||
}
|
||||
#endif
|
||||
if (drawBubbleOutline) {
|
||||
display->drawRect(bubbleX, topY, bubbleW, bubbleH);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // end if (showBubbles)
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
if (useDarkModeBubbleFill) {
|
||||
// Restore theme role defaults so other screens keep their intended palette.
|
||||
loadThemeDefaults();
|
||||
}
|
||||
#endif
|
||||
|
||||
// Render visible lines
|
||||
int lineY = yOffset;
|
||||
@@ -772,7 +891,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
headerX = x + textIndent;
|
||||
}
|
||||
graphics::UIRenderer::drawStringWithEmotes(display, headerX, lineY, cachedLines[i].c_str(), FONT_HEIGHT_SMALL, 1,
|
||||
false);
|
||||
true);
|
||||
|
||||
// Draw underline just under header text
|
||||
int underlineY = lineY + FONT_HEIGHT_SMALL;
|
||||
@@ -953,12 +1072,12 @@ void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const mesht
|
||||
// Banner logic
|
||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet.from);
|
||||
char longName[64] = "?";
|
||||
if (node && node->has_user) {
|
||||
if (node->user.long_name[0]) {
|
||||
strncpy(longName, node->user.long_name, sizeof(longName) - 1);
|
||||
if (nodeInfoLiteHasUser(node)) {
|
||||
if (node->long_name[0]) {
|
||||
strncpy(longName, node->long_name, sizeof(longName) - 1);
|
||||
longName[sizeof(longName) - 1] = '\0';
|
||||
} else if (node->user.short_name[0]) {
|
||||
strncpy(longName, node->user.short_name, sizeof(longName) - 1);
|
||||
} else if (node->short_name[0]) {
|
||||
strncpy(longName, node->short_name, sizeof(longName) - 1);
|
||||
longName[sizeof(longName) - 1] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,16 @@
|
||||
#include "CompassRenderer.h"
|
||||
#include "NodeDB.h"
|
||||
#include "NodeListRenderer.h"
|
||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||
#include "modules/StatusMessageModule.h"
|
||||
#endif
|
||||
#include "UIRenderer.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
#include "gps/RTC.h" // for getTime() function
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/TFTColorRegions.h"
|
||||
#include "graphics/TFTPalette.h"
|
||||
#include "graphics/images.h"
|
||||
#include "meshUtils.h"
|
||||
#include <algorithm>
|
||||
@@ -92,8 +97,35 @@ std::string getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node,
|
||||
|
||||
// 1) Choose target candidate (long vs short) only if present
|
||||
const char *raw = nullptr;
|
||||
if (node && node->has_user) {
|
||||
raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name;
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_STATUS && !MESHTASTIC_EXCLUDE_STATUSDB
|
||||
// If long-name mode is enabled, and we have a recent status for this node,
|
||||
// prefer "(short_name) statusText" as the raw candidate. Pull straight out
|
||||
// of NodeDB's per-NodeNum cache instead of scanning a FIFO.
|
||||
std::string composedFromStatus;
|
||||
if (config.display.use_long_node_name && nodeInfoLiteHasUser(node) && nodeDB) {
|
||||
meshtastic_StatusMessage cachedStatus;
|
||||
if (nodeDB->copyNodeStatus(node->num, cachedStatus) && cachedStatus.status[0]) {
|
||||
const char *shortName = node->short_name;
|
||||
const size_t statusLen = std::strlen(cachedStatus.status);
|
||||
composedFromStatus.reserve(4 + (shortName ? std::strlen(shortName) : 0) + 1 + statusLen);
|
||||
composedFromStatus += "(";
|
||||
if (shortName && *shortName) {
|
||||
composedFromStatus += shortName;
|
||||
}
|
||||
composedFromStatus += ") ";
|
||||
composedFromStatus += cachedStatus.status;
|
||||
|
||||
raw = composedFromStatus.c_str(); // safe for now; we'll sanitize immediately into std::string
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// If we didn't compose from status, use normal long/short selection
|
||||
if (!raw) {
|
||||
if (nodeInfoLiteHasUser(node)) {
|
||||
raw = config.display.use_long_node_name ? node->long_name : node->short_name;
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Preserve UTF-8 names so emotes can be detected and rendered.
|
||||
@@ -177,6 +209,33 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries,
|
||||
}
|
||||
}
|
||||
|
||||
static inline void applyFavoriteNodeNameColor(OLEDDisplay *display, const meshtastic_NodeInfoLite *node, const char *nodeName,
|
||||
int16_t nameX, int16_t y, int nameMaxWidth)
|
||||
{
|
||||
if (!display || !node || !nodeInfoLiteIsFavorite(node) || !isTFTColoringEnabled() || !nodeName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int textWidth = UIRenderer::measureStringWithEmotes(display, nodeName);
|
||||
const int regionWidth = min(textWidth, max(0, nameMaxWidth));
|
||||
if (regionWidth <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Node list rows can begin a couple of pixels inside header space.
|
||||
// Clamp favorite-name color region below the header to avoid black overlap there.
|
||||
const int16_t minContentY = static_cast<int16_t>(FONT_HEIGHT_SMALL + 1);
|
||||
const int16_t regionY = max(y, minContentY);
|
||||
const int16_t yClip = regionY - y;
|
||||
const int16_t regionHeight = static_cast<int16_t>(FONT_HEIGHT_SMALL - yClip);
|
||||
if (regionHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setAndRegisterTFTColorRole(TFTColorRole::FavoriteNode, TFTPalette::Yellow, TFTPalette::Black, nameX, regionY, regionWidth,
|
||||
regionHeight);
|
||||
}
|
||||
|
||||
// =============================
|
||||
// Entry Renderers
|
||||
// =============================
|
||||
@@ -191,7 +250,10 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
||||
char nodeName[96];
|
||||
UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName),
|
||||
nameMaxWidth);
|
||||
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
|
||||
#endif
|
||||
bool isMuted = nodeInfoLiteIsMuted(node);
|
||||
|
||||
char timeStr[10];
|
||||
uint32_t seconds = sinceLastSeen(node);
|
||||
@@ -211,14 +273,14 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false);
|
||||
if (node->is_favorite) {
|
||||
if (nodeInfoLiteIsFavorite(node)) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||
} else {
|
||||
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||
}
|
||||
}
|
||||
if (node->is_ignored || isMuted) {
|
||||
if (nodeInfoLiteIsIgnored(node) || isMuted) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
|
||||
} else {
|
||||
@@ -239,28 +301,34 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
||||
|
||||
int nameMaxWidth = getNodeNameMaxWidth(columnWidth, columnWidth - 25);
|
||||
int barsOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19);
|
||||
int hopOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17);
|
||||
constexpr int kBarCount = 4;
|
||||
constexpr int kBarWidth = 2;
|
||||
constexpr int kBarGap = 1;
|
||||
|
||||
int barsXOffset = columnWidth - barsOffset;
|
||||
int barsRightEdge = x + barsXOffset + ((kBarCount - 1) * (kBarWidth + kBarGap)) + kBarWidth;
|
||||
|
||||
const int nameX = x + ((currentResolution == ScreenResolution::High) ? 6 : 3);
|
||||
char nodeName[96];
|
||||
UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName),
|
||||
nameMaxWidth);
|
||||
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
|
||||
#endif
|
||||
bool isMuted = nodeInfoLiteIsMuted(node);
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
|
||||
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false);
|
||||
if (node->is_favorite) {
|
||||
if (nodeInfoLiteIsFavorite(node)) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||
} else {
|
||||
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||
}
|
||||
}
|
||||
if (node->is_ignored || isMuted) {
|
||||
if (nodeInfoLiteIsIgnored(node) || isMuted) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
|
||||
} else {
|
||||
@@ -268,28 +336,48 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
||||
}
|
||||
}
|
||||
|
||||
// Draw signal strength bars
|
||||
int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0;
|
||||
int barWidth = 2;
|
||||
int barStartX = x + barsXOffset;
|
||||
int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2;
|
||||
const bool isZeroHop = node->has_hops_away && node->hops_away == 0;
|
||||
|
||||
for (int b = 0; b < 4; b++) {
|
||||
if (b < bars) {
|
||||
int height = (b * 2);
|
||||
display->fillRect(barStartX + (b * (barWidth + 1)), barStartY - height, barWidth, height);
|
||||
// Show signal only for direct neighbors (0 hops)
|
||||
if (isZeroHop) {
|
||||
int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0;
|
||||
int barStartX = x + barsXOffset;
|
||||
int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2;
|
||||
|
||||
if (bars > 0) {
|
||||
uint16_t signalBarsColor = TFTPalette::Bad;
|
||||
if (bars >= 3) {
|
||||
signalBarsColor = TFTPalette::Good;
|
||||
} else if (bars == 2) {
|
||||
signalBarsColor = TFTPalette::Medium;
|
||||
}
|
||||
|
||||
// Highest bar reaches 6 px in this renderer.
|
||||
setAndRegisterTFTColorRole(TFTColorRole::SignalBars, signalBarsColor, TFTPalette::Black, barStartX, barStartY - 6,
|
||||
(kBarCount * kBarWidth) + ((kBarCount - 1) * kBarGap), 6);
|
||||
}
|
||||
|
||||
for (int b = 0; b < kBarCount; b++) {
|
||||
if (b < bars) {
|
||||
int height = (b * 2);
|
||||
display->fillRect(barStartX + (b * (kBarWidth + kBarGap)), barStartY - height, kBarWidth, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw hop count
|
||||
char hopStr[6] = "";
|
||||
if (node->has_hops_away && node->hops_away > 0)
|
||||
snprintf(hopStr, sizeof(hopStr), "[%d]", node->hops_away);
|
||||
// Draw hop count + hop icon
|
||||
if (node->has_hops_away && node->hops_away > 0) {
|
||||
char hopCount[6];
|
||||
snprintf(hopCount, sizeof(hopCount), "%d", node->hops_away);
|
||||
|
||||
if (hopStr[0] != '\0') {
|
||||
int rightEdge = x + columnWidth - hopOffset;
|
||||
int textWidth = display->getStringWidth(hopStr);
|
||||
display->drawString(rightEdge - textWidth, y, hopStr);
|
||||
const int hopCountWidth = display->getStringWidth(hopCount);
|
||||
const int gap = 1;
|
||||
const int totalWidth = hopCountWidth + gap + hop_width;
|
||||
const int hopX = barsRightEdge - totalWidth;
|
||||
const int iconY = y + (FONT_HEIGHT_SMALL - hop_height) / 2;
|
||||
|
||||
display->drawString(hopX, y, hopCount);
|
||||
display->drawXbm(hopX + hopCountWidth + gap, iconY, hop_width, hop_height, hop);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,15 +392,22 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
char nodeName[96];
|
||||
UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName),
|
||||
nameMaxWidth);
|
||||
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
|
||||
#endif
|
||||
bool isMuted = nodeInfoLiteIsMuted(node);
|
||||
char distStr[10] = "";
|
||||
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) {
|
||||
double lat1 = ourNode->position.latitude_i * 1e-7;
|
||||
double lon1 = ourNode->position.longitude_i * 1e-7;
|
||||
double lat2 = node->position.latitude_i * 1e-7;
|
||||
double lon2 = node->position.longitude_i * 1e-7;
|
||||
const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
meshtastic_PositionLite ourPos;
|
||||
meshtastic_PositionLite theirPos;
|
||||
const bool haveOurPos = ourNode && nodeDB->copyNodePosition(ourNode->num, ourPos);
|
||||
const bool haveTheirPos = nodeDB->copyNodePosition(node->num, theirPos);
|
||||
if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node) && haveOurPos && haveTheirPos) {
|
||||
double lat1 = ourPos.latitude_i * 1e-7;
|
||||
double lon1 = ourPos.longitude_i * 1e-7;
|
||||
double lat2 = theirPos.latitude_i * 1e-7;
|
||||
double lon2 = theirPos.longitude_i * 1e-7;
|
||||
|
||||
double earthRadiusKm = 6371.0;
|
||||
double dLat = (lat2 - lat1) * DEG_TO_RAD;
|
||||
@@ -358,14 +453,14 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false);
|
||||
if (node->is_favorite) {
|
||||
if (nodeInfoLiteIsFavorite(node)) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||
} else {
|
||||
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||
}
|
||||
}
|
||||
if (node->is_ignored || isMuted) {
|
||||
if (nodeInfoLiteIsIgnored(node) || isMuted) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
|
||||
} else {
|
||||
@@ -373,14 +468,13 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
}
|
||||
}
|
||||
|
||||
if (strlen(distStr) > 0) {
|
||||
int offset = (currentResolution == ScreenResolution::High)
|
||||
? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column)
|
||||
: (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column)
|
||||
int rightEdge = x + columnWidth - offset;
|
||||
int textWidth = display->getStringWidth(distStr);
|
||||
display->drawString(rightEdge - textWidth, y, distStr);
|
||||
}
|
||||
const char *distanceLabel = (strlen(distStr) > 0) ? distStr : "?";
|
||||
int offset = (currentResolution == ScreenResolution::High)
|
||||
? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column)
|
||||
: (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column)
|
||||
int rightEdge = x + columnWidth - offset;
|
||||
int textWidth = display->getStringWidth(distanceLabel);
|
||||
display->drawString(rightEdge - textWidth, y, distanceLabel);
|
||||
}
|
||||
|
||||
void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
|
||||
@@ -410,19 +504,22 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
char nodeName[96];
|
||||
UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName),
|
||||
nameMaxWidth);
|
||||
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
|
||||
#endif
|
||||
bool isMuted = nodeInfoLiteIsMuted(node);
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false);
|
||||
if (node->is_favorite) {
|
||||
if (nodeInfoLiteIsFavorite(node)) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||
} else {
|
||||
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||
}
|
||||
}
|
||||
if (node->is_ignored || isMuted) {
|
||||
if (nodeInfoLiteIsIgnored(node) || isMuted) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
|
||||
} else {
|
||||
@@ -431,8 +528,8 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
}
|
||||
}
|
||||
|
||||
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading,
|
||||
double userLat, double userLon)
|
||||
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth,
|
||||
float myHeadingRadian, double userLat, double userLon)
|
||||
{
|
||||
if (!nodeDB->hasValidPosition(node))
|
||||
return;
|
||||
@@ -443,14 +540,17 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
int centerX = x + columnWidth - arrowXOffset;
|
||||
int centerY = y + FONT_HEIGHT_SMALL / 2;
|
||||
|
||||
double nodeLat = node->position.latitude_i * 1e-7;
|
||||
double nodeLon = node->position.longitude_i * 1e-7;
|
||||
meshtastic_PositionLite nodePos;
|
||||
if (!nodeDB->copyNodePosition(node->num, nodePos))
|
||||
return;
|
||||
double nodeLat = nodePos.latitude_i * 1e-7;
|
||||
double nodeLon = nodePos.longitude_i * 1e-7;
|
||||
float bearing = GeoCoord::bearing(userLat, userLon, nodeLat, nodeLon);
|
||||
float bearingToNode = RAD_TO_DEG * bearing;
|
||||
float relativeBearing = fmod((bearingToNode - myHeading + 360), 360);
|
||||
float relativeBearing = CompassRenderer::adjustBearingForCompassMode(bearing, myHeadingRadian);
|
||||
float relativeBearingDeg = CompassRenderer::radiansToDegrees360(relativeBearing);
|
||||
// Shrink size by 2px
|
||||
int size = FONT_HEIGHT_SMALL - 5;
|
||||
CompassRenderer::drawArrowToNode(display, centerX, centerY, size, relativeBearing);
|
||||
CompassRenderer::drawArrowToNode(display, centerX, centerY, size, relativeBearingDeg);
|
||||
/*
|
||||
float angle = relativeBearing * DEG_TO_RAD;
|
||||
float halfSize = size / 2.0;
|
||||
@@ -480,12 +580,27 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
*/
|
||||
}
|
||||
|
||||
void drawCompassUnknown(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float, double,
|
||||
double)
|
||||
{
|
||||
if (!nodeDB->hasValidPosition(node))
|
||||
return;
|
||||
|
||||
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||
int arrowXOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18);
|
||||
int centerX = x + columnWidth - arrowXOffset;
|
||||
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->drawString(centerX, y, "?");
|
||||
}
|
||||
|
||||
// =============================
|
||||
// Main Screen Functions
|
||||
// =============================
|
||||
|
||||
void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title,
|
||||
EntryRenderer renderer, NodeExtrasRenderer extras, float heading, double lat, double lon)
|
||||
EntryRenderer renderer, NodeExtrasRenderer extras, float headingRadian, double lat, double lon)
|
||||
{
|
||||
const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1;
|
||||
const int rowYOffset = FONT_HEIGHT_SMALL - 3;
|
||||
@@ -538,7 +653,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
continue;
|
||||
if (n->num == nodeDB->getNodeNum())
|
||||
continue;
|
||||
if (locationScreen && !n->has_position)
|
||||
if (locationScreen && !nodeDB->hasNodePosition(n->num))
|
||||
continue;
|
||||
|
||||
drawList.push_back(n->num);
|
||||
@@ -570,7 +685,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
renderer(display, node, xPos, yPos, columnWidth);
|
||||
|
||||
if (extras)
|
||||
extras(display, node, xPos, yPos, columnWidth, heading, lat, lon);
|
||||
extras(display, node, xPos, yPos, columnWidth, headingRadian, lat, lon);
|
||||
|
||||
lastNodeY = max(lastNodeY, yPos + FONT_HEIGHT_SMALL);
|
||||
yOffset += rowYOffset;
|
||||
@@ -650,6 +765,9 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1);
|
||||
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
|
||||
display->setColor(WHITE);
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
registerTFTActionMenuRegions(boxLeft, boxTop, boxWidth, boxHeight);
|
||||
#endif
|
||||
|
||||
// Text
|
||||
display->drawString(boxLeft + padding, boxTop + padding, buf);
|
||||
@@ -765,11 +883,16 @@ void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
#endif
|
||||
void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
float heading = 0;
|
||||
bool validHeading = false;
|
||||
auto ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
double lat = DegD(ourNode->position.latitude_i);
|
||||
double lon = DegD(ourNode->position.longitude_i);
|
||||
float headingRadian = 0.0f;
|
||||
const auto *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
meshtastic_PositionLite ourSelfPos;
|
||||
if (!ourNode || !nodeDB->hasValidPosition(ourNode) || !nodeDB->copyNodePosition(ourNode->num, ourSelfPos)) {
|
||||
drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassUnknown, headingRadian, 0.0, 0.0);
|
||||
return;
|
||||
}
|
||||
|
||||
double lat = DegD(ourSelfPos.latitude_i);
|
||||
double lon = DegD(ourSelfPos.longitude_i);
|
||||
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
display->clear();
|
||||
@@ -779,21 +902,12 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state,
|
||||
lastSwitchTime = now;
|
||||
}
|
||||
#endif
|
||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) {
|
||||
#if HAS_GPS
|
||||
if (screen->hasHeading()) {
|
||||
heading = screen->getHeading(); // degrees
|
||||
validHeading = true;
|
||||
} else {
|
||||
heading = screen->estimatedHeading(lat, lon);
|
||||
validHeading = !isnan(heading);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!validHeading)
|
||||
return;
|
||||
if (!CompassRenderer::getHeadingRadians(lat, lon, headingRadian)) {
|
||||
drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassUnknown, headingRadian, lat, lon);
|
||||
return;
|
||||
}
|
||||
drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, heading, lat, lon);
|
||||
|
||||
drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, headingRadian, lat, lon);
|
||||
}
|
||||
|
||||
/// Draw a series of fields in a column, wrapping to multiple columns if needed
|
||||
|
||||
@@ -32,7 +32,7 @@ enum ListMode_Location { MODE_DISTANCE = 0, MODE_BEARING = 1, MODE_COUNT_LOCATIO
|
||||
|
||||
// Main node list screen function
|
||||
void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title,
|
||||
EntryRenderer renderer, NodeExtrasRenderer extras = nullptr, float heading = 0, double lat = 0,
|
||||
EntryRenderer renderer, NodeExtrasRenderer extras = nullptr, float headingRadian = 0, double lat = 0,
|
||||
double lon = 0);
|
||||
|
||||
// Entry renderers
|
||||
@@ -43,8 +43,8 @@ void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node,
|
||||
void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
|
||||
|
||||
// Extras renderers
|
||||
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading,
|
||||
double userLat, double userLon);
|
||||
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth,
|
||||
float myHeadingRadian, double userLat, double userLon);
|
||||
|
||||
// Screen frame functions
|
||||
void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
#include "UIRenderer.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/TFTColorRegions.h"
|
||||
#include "graphics/TFTPalette.h"
|
||||
#include "graphics/images.h"
|
||||
#include "input/RotaryEncoderInterruptImpl1.h"
|
||||
#include "input/UpDownInterruptImpl1.h"
|
||||
@@ -64,6 +66,110 @@ uint32_t pow_of_10(uint32_t n)
|
||||
return ret;
|
||||
}
|
||||
|
||||
char graphics::NotificationRenderer::alertBannerLines[MAX_LINES + 1][64] = {};
|
||||
uint8_t graphics::NotificationRenderer::alertBannerLineCount = 0;
|
||||
graphics::NotificationRenderer::BannerFont graphics::NotificationRenderer::alertBannerLineFonts[MAX_LINES + 1] = {};
|
||||
|
||||
static inline graphics::NotificationRenderer::BannerFont parseFontTagPrefix(const char *&p)
|
||||
{
|
||||
// Tags must be at the start of the line:
|
||||
// [S] small, [M] medium, [L] large
|
||||
if (p && p[0] == '[' && p[2] == ']' && p[1] != '\0') {
|
||||
char t = p[1];
|
||||
if (t == 'S') {
|
||||
p += 3;
|
||||
return graphics::NotificationRenderer::BANNER_FONT_SMALL;
|
||||
}
|
||||
if (t == 'M') {
|
||||
p += 3;
|
||||
return graphics::NotificationRenderer::BANNER_FONT_MEDIUM;
|
||||
}
|
||||
if (t == 'L') {
|
||||
p += 3;
|
||||
return graphics::NotificationRenderer::BANNER_FONT_LARGE;
|
||||
}
|
||||
}
|
||||
return graphics::NotificationRenderer::BANNER_FONT_DEFAULT;
|
||||
}
|
||||
|
||||
static inline const uint8_t *fontForBannerLine(graphics::NotificationRenderer::BannerFont f)
|
||||
{
|
||||
switch (f) {
|
||||
case graphics::NotificationRenderer::BANNER_FONT_SMALL:
|
||||
return FONT_SMALL;
|
||||
case graphics::NotificationRenderer::BANNER_FONT_MEDIUM:
|
||||
return FONT_MEDIUM;
|
||||
case graphics::NotificationRenderer::BANNER_FONT_LARGE:
|
||||
return FONT_LARGE;
|
||||
case graphics::NotificationRenderer::BANNER_FONT_DEFAULT:
|
||||
default:
|
||||
return FONT_SMALL;
|
||||
}
|
||||
}
|
||||
|
||||
static inline uint8_t effectiveLineHeightForBannerLine(graphics::NotificationRenderer::BannerFont f)
|
||||
{
|
||||
uint8_t height = FONT_HEIGHT_SMALL;
|
||||
switch (f) {
|
||||
case graphics::NotificationRenderer::BANNER_FONT_MEDIUM:
|
||||
height = FONT_HEIGHT_MEDIUM;
|
||||
break;
|
||||
case graphics::NotificationRenderer::BANNER_FONT_LARGE:
|
||||
height = FONT_HEIGHT_LARGE;
|
||||
break;
|
||||
case graphics::NotificationRenderer::BANNER_FONT_SMALL:
|
||||
case graphics::NotificationRenderer::BANNER_FONT_DEFAULT:
|
||||
default:
|
||||
height = FONT_HEIGHT_SMALL;
|
||||
break;
|
||||
}
|
||||
return (height > 3) ? (height - 3) : height;
|
||||
}
|
||||
|
||||
void graphics::NotificationRenderer::parseBannerMessageWithFonts(const char *message)
|
||||
{
|
||||
alertBannerLineCount = 0;
|
||||
for (uint8_t i = 0; i < (MAX_LINES + 1); i++) {
|
||||
alertBannerLines[i][0] = '\0';
|
||||
alertBannerLineFonts[i] = BANNER_FONT_DEFAULT;
|
||||
}
|
||||
|
||||
if (!message || !message[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char *p = message;
|
||||
|
||||
while (*p && alertBannerLineCount < (MAX_LINES + 1)) {
|
||||
const char *lineStart = p;
|
||||
while (*p && *p != '\n') {
|
||||
p++;
|
||||
}
|
||||
|
||||
char tmp[64] = {0};
|
||||
size_t len = (size_t)(p - lineStart);
|
||||
if (len > (sizeof(tmp) - 1)) {
|
||||
len = sizeof(tmp) - 1;
|
||||
}
|
||||
memcpy(tmp, lineStart, len);
|
||||
tmp[len] = '\0';
|
||||
|
||||
// Tag at start
|
||||
const char *tp = tmp;
|
||||
BannerFont f = parseFontTagPrefix(tp);
|
||||
alertBannerLineFonts[alertBannerLineCount] = f;
|
||||
|
||||
// Store stripped text
|
||||
strncpy(alertBannerLines[alertBannerLineCount], tp, sizeof(alertBannerLines[0]) - 1);
|
||||
alertBannerLines[alertBannerLineCount][sizeof(alertBannerLines[0]) - 1] = '\0';
|
||||
alertBannerLineCount++;
|
||||
|
||||
if (*p == '\n') {
|
||||
p++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Used on boot when a certificate is being created
|
||||
void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
@@ -315,12 +421,12 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
|
||||
for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) {
|
||||
char tempName[48] = {0};
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i + 1);
|
||||
if (node && node->has_user) {
|
||||
if (nodeInfoLiteHasUser(node)) {
|
||||
const char *rawName = nullptr;
|
||||
if (node->user.long_name[0]) {
|
||||
rawName = node->user.long_name;
|
||||
} else if (node->user.short_name[0]) {
|
||||
rawName = node->user.short_name;
|
||||
if (node->long_name[0]) {
|
||||
rawName = node->long_name;
|
||||
} else if (node->short_name[0]) {
|
||||
rawName = node->short_name;
|
||||
}
|
||||
if (rawName) {
|
||||
const int arrowWidth = (currentResolution == ScreenResolution::High)
|
||||
@@ -373,23 +479,37 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
|
||||
const char *lineStarts[MAX_LINES + 1] = {0};
|
||||
uint16_t lineCount = 0;
|
||||
char lineBuffer[40] = {0};
|
||||
bool useTaggedTextBanner =
|
||||
(current_notification_type == notificationTypeEnum::text_banner && alertBannerOptions == 0 && alertBannerLineCount > 0);
|
||||
|
||||
// Parse lines
|
||||
char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage));
|
||||
lineStarts[lineCount] = alertBannerMessage;
|
||||
if (useTaggedTextBanner) {
|
||||
lineCount = std::min<uint8_t>(alertBannerLineCount, MAX_LINES);
|
||||
for (uint16_t i = 0; i < lineCount; i++) {
|
||||
lineStarts[i] = alertBannerLines[i];
|
||||
lineLengths[i] = strlen(lineStarts[i]);
|
||||
display->setFont(fontForBannerLine(alertBannerLineFonts[i]));
|
||||
lineWidths[i] = display->getStringWidth(lineStarts[i], lineLengths[i], true);
|
||||
if (lineWidths[i] > maxWidth)
|
||||
maxWidth = lineWidths[i];
|
||||
}
|
||||
} else {
|
||||
char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage));
|
||||
lineStarts[lineCount] = alertBannerMessage;
|
||||
|
||||
while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) {
|
||||
lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n');
|
||||
lineLengths[lineCount] = lineStarts[lineCount + 1] - lineStarts[lineCount];
|
||||
if (lineStarts[lineCount + 1][0] == '\n')
|
||||
lineStarts[lineCount + 1] += 1;
|
||||
lineWidths[lineCount] = display->getStringWidth(lineStarts[lineCount], lineLengths[lineCount], true);
|
||||
if (lineWidths[lineCount] > maxWidth)
|
||||
maxWidth = lineWidths[lineCount];
|
||||
lineCount++;
|
||||
while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) {
|
||||
lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n');
|
||||
lineLengths[lineCount] = lineStarts[lineCount + 1] - lineStarts[lineCount];
|
||||
if (lineStarts[lineCount + 1][0] == '\n')
|
||||
lineStarts[lineCount + 1] += 1;
|
||||
lineWidths[lineCount] = display->getStringWidth(lineStarts[lineCount], lineLengths[lineCount], true);
|
||||
if (lineWidths[lineCount] > maxWidth)
|
||||
maxWidth = lineWidths[lineCount];
|
||||
lineCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Measure option widths
|
||||
display->setFont(FONT_SMALL);
|
||||
for (int i = 0; i < alertBannerOptions; i++) {
|
||||
optionWidths[i] = display->getStringWidth(optionsArrayPtr[i], strlen(optionsArrayPtr[i]), true);
|
||||
if (optionWidths[i] > maxWidth)
|
||||
@@ -503,6 +623,10 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
bool needs_bell = false;
|
||||
uint16_t lineWidths[totalLines] = {0};
|
||||
uint16_t lineLengths[totalLines] = {0};
|
||||
BannerFont lineFonts[totalLines] = {};
|
||||
uint8_t lineEffectiveHeights[totalLines] = {0};
|
||||
const char *renderLines[totalLines] = {0};
|
||||
bool useTaggedBannerFonts = (current_notification_type == notificationTypeEnum::text_banner && alertBannerOptions == 0);
|
||||
|
||||
if (maxWidth != 0)
|
||||
is_picker = true;
|
||||
@@ -515,11 +639,22 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
uint16_t widestLineWithBars = 0;
|
||||
|
||||
while (lines[lineCount] != nullptr) {
|
||||
auto newlinePointer = strchr(lines[lineCount], '\n');
|
||||
const char *renderText = lines[lineCount];
|
||||
BannerFont lineFont = BANNER_FONT_DEFAULT;
|
||||
if (useTaggedBannerFonts && lineCount < alertBannerLineCount) {
|
||||
renderText = alertBannerLines[lineCount];
|
||||
lineFont = alertBannerLineFonts[lineCount];
|
||||
}
|
||||
renderLines[lineCount] = renderText;
|
||||
lineFonts[lineCount] = lineFont;
|
||||
lineEffectiveHeights[lineCount] = effectiveLineHeightForBannerLine(lineFont);
|
||||
display->setFont(fontForBannerLine(lineFont));
|
||||
|
||||
auto newlinePointer = strchr(renderText, '\n');
|
||||
if (newlinePointer)
|
||||
lineLengths[lineCount] = (newlinePointer - lines[lineCount]); // Check for newlines first
|
||||
else // if the newline wasn't found, then pull string length from strlen
|
||||
lineLengths[lineCount] = strlen(lines[lineCount]);
|
||||
lineLengths[lineCount] = (newlinePointer - renderText);
|
||||
else
|
||||
lineLengths[lineCount] = strlen(renderText);
|
||||
|
||||
if (current_notification_type == notificationTypeEnum::node_picker) {
|
||||
char measureBuffer[64] = {0};
|
||||
@@ -531,7 +666,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
|
||||
// Consider extra width for signal bars on lines that contain "Signal:"
|
||||
uint16_t potentialWidth = lineWidths[lineCount];
|
||||
if (graphics::bannerSignalBars >= 0 && strncmp(lines[lineCount], "Signal:", 7) == 0) {
|
||||
if (graphics::bannerSignalBars >= 0 && strncmp(renderText, "Signal:", 7) == 0) {
|
||||
const int totalBars = 5;
|
||||
const int barWidth = 3;
|
||||
const int barSpacing = 2;
|
||||
@@ -567,8 +702,21 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
|
||||
uint16_t screenHeight = display->height();
|
||||
uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3;
|
||||
uint8_t visibleTotalLines = std::min<uint8_t>(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight);
|
||||
uint16_t contentHeight = visibleTotalLines * effectiveLineHeight;
|
||||
uint8_t visibleTotalLines = 0;
|
||||
uint16_t contentHeight = 0;
|
||||
const uint16_t availableHeight = (screenHeight > (vPadding * 2)) ? (screenHeight - vPadding * 2) : 0;
|
||||
for (uint8_t i = 0; i < lineCount; i++) {
|
||||
uint8_t thisLineHeight = lineEffectiveHeights[i] ? lineEffectiveHeights[i] : effectiveLineHeight;
|
||||
if (contentHeight + thisLineHeight > availableHeight) {
|
||||
break;
|
||||
}
|
||||
contentHeight += thisLineHeight;
|
||||
visibleTotalLines++;
|
||||
}
|
||||
if (visibleTotalLines == 0 && lineCount > 0) {
|
||||
visibleTotalLines = 1;
|
||||
contentHeight = lineEffectiveHeights[0] ? lineEffectiveHeights[0] : effectiveLineHeight;
|
||||
}
|
||||
uint16_t boxHeight = contentHeight + vPadding * 2;
|
||||
if (visibleTotalLines == 1) {
|
||||
boxHeight += (currentResolution == ScreenResolution::High) ? 4 : 3;
|
||||
@@ -608,18 +756,24 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1);
|
||||
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
|
||||
display->setColor(WHITE);
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
registerTFTActionMenuRegions(boxLeft, boxTop, boxWidth, boxHeight);
|
||||
#endif
|
||||
|
||||
// Draw Content
|
||||
int16_t lineY = boxTop + vPadding;
|
||||
for (int i = 0; i < lineCount; i++) {
|
||||
for (int i = 0; i < visibleTotalLines; i++) {
|
||||
display->setFont(fontForBannerLine(lineFonts[i]));
|
||||
int16_t thisLineHeight = lineEffectiveHeights[i] ? lineEffectiveHeights[i] : effectiveLineHeight;
|
||||
int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2;
|
||||
if (needs_bell && i == 0) {
|
||||
int bellY = lineY + (FONT_HEIGHT_SMALL - 8) / 2;
|
||||
int fontHeight = thisLineHeight + 3;
|
||||
int bellY = lineY + (fontHeight - 8) / 2;
|
||||
display->drawXbm(textX - 10, bellY, 8, 8, bell_alert);
|
||||
display->drawXbm(textX + lineWidths[i] + 2, bellY, 8, 8, bell_alert);
|
||||
}
|
||||
char lineBuffer[lineLengths[i] + 1];
|
||||
strncpy(lineBuffer, lines[i], lineLengths[i]);
|
||||
strncpy(lineBuffer, renderLines[i], lineLengths[i]);
|
||||
lineBuffer[lineLengths[i]] = '\0';
|
||||
// Determine if this is a pop-up or a pick list
|
||||
if (alertBannerOptions > 0 && i == 0) {
|
||||
@@ -630,7 +784,21 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
if (strchr(lineBuffer, 'p') || strchr(lineBuffer, 'g') || strchr(lineBuffer, 'y') || strchr(lineBuffer, 'j')) {
|
||||
background_yOffset = -1;
|
||||
}
|
||||
display->fillRect(boxLeft, boxTop + 1, boxWidth, effectiveLineHeight - background_yOffset);
|
||||
const int16_t titleBarY = boxTop + 1;
|
||||
const int16_t titleBarHeight = effectiveLineHeight - background_yOffset;
|
||||
display->fillRect(boxLeft, titleBarY, boxWidth, titleBarHeight);
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
if (alertBannerOptions > 0) {
|
||||
const uint16_t titleTextColor =
|
||||
(getActiveTheme().id == ThemeID::DefaultLight) ? TFTPalette::Black : getThemeHeaderText();
|
||||
// Keep title role away from border/corner pixels so rounded-corner masks are not remapped to the title text
|
||||
// color.
|
||||
if (boxWidth > 2 && titleBarHeight > 0) {
|
||||
setAndRegisterTFTColorRole(TFTColorRole::ActionMenuTitle, getThemeHeaderBg(), titleTextColor, boxLeft + 1,
|
||||
titleBarY, boxWidth - 2, titleBarHeight);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
display->setColor(BLACK);
|
||||
int yOffset = 3;
|
||||
if (current_notification_type == notificationTypeEnum::node_picker) {
|
||||
@@ -639,7 +807,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
display->drawString(textX, lineY - yOffset, lineBuffer);
|
||||
}
|
||||
display->setColor(WHITE);
|
||||
lineY += (effectiveLineHeight - 2 - background_yOffset);
|
||||
lineY += (thisLineHeight - 2 - background_yOffset);
|
||||
} else {
|
||||
// Pop-up
|
||||
// If this is the Signal line, center text + bars as one group
|
||||
@@ -650,6 +818,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
const int barSpacing = 2;
|
||||
const int barHeightStep = 2;
|
||||
const int gap = 6;
|
||||
const int maxBarHeight = totalBars * barHeightStep;
|
||||
|
||||
int textWidth = display->getStringWidth(lineBuffer, strlen(lineBuffer), true);
|
||||
int barsWidth = totalBars * barWidth + (totalBars - 1) * barSpacing + gap;
|
||||
@@ -664,6 +833,20 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
|
||||
int baseX = groupStartX + textWidth + gap;
|
||||
int baseY = lineY + effectiveLineHeight - 1;
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
if (graphics::bannerSignalBars > 0) {
|
||||
uint16_t signalBarsColor = TFTPalette::Medium;
|
||||
if (graphics::bannerSignalBars <= 1) {
|
||||
signalBarsColor = TFTPalette::Bad;
|
||||
} else if (graphics::bannerSignalBars >= 4) {
|
||||
signalBarsColor = TFTPalette::Good;
|
||||
}
|
||||
const int activeBars = min(graphics::bannerSignalBars, totalBars);
|
||||
const int regionWidth = activeBars * barWidth + (activeBars - 1) * barSpacing;
|
||||
setAndRegisterTFTColorRole(TFTColorRole::SignalBars, signalBarsColor, TFTPalette::Black, baseX,
|
||||
baseY - maxBarHeight, regionWidth, maxBarHeight);
|
||||
}
|
||||
#endif
|
||||
for (int b = 0; b < totalBars; b++) {
|
||||
int barHeight = (b + 1) * barHeightStep;
|
||||
int x = baseX + b * (barWidth + barSpacing);
|
||||
@@ -682,7 +865,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
display->drawString(textX, lineY, lineBuffer);
|
||||
}
|
||||
}
|
||||
lineY += (effectiveLineHeight);
|
||||
lineY += thisLineHeight;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,12 @@ class NotificationRenderer
|
||||
|
||||
static bool pauseBanner;
|
||||
|
||||
enum BannerFont : uint8_t { BANNER_FONT_DEFAULT = 0, BANNER_FONT_SMALL, BANNER_FONT_MEDIUM, BANNER_FONT_LARGE };
|
||||
|
||||
static char alertBannerLines[MAX_LINES + 1][64]; // parsed text per line
|
||||
static uint8_t alertBannerLineCount;
|
||||
static BannerFont alertBannerLineFonts[MAX_LINES + 1];
|
||||
static void parseBannerMessageWithFonts(const char *message);
|
||||
static void resetBanner();
|
||||
static void showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs);
|
||||
static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
|
||||
+792
-355
File diff suppressed because it is too large
Load Diff
@@ -50,10 +50,12 @@ class UIRenderer
|
||||
// Navigation bar overlay
|
||||
static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
|
||||
static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
static void drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
static void drawBootIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
// Icon and screen drawing functions
|
||||
static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
|
||||
@@ -25,6 +25,116 @@
|
||||
|
||||
using namespace NicheGraphics::Drivers;
|
||||
|
||||
#if defined(T5_S3_EPAPER_PRO_V2)
|
||||
// FastEPD helper symbols are defined in FastEPD.inl with C++ linkage.
|
||||
extern void bbepPCA9535DigitalWrite(uint8_t pin, uint8_t value);
|
||||
extern uint8_t bbepPCA9535DigitalRead(uint8_t pin);
|
||||
extern int bbepI2CWrite(unsigned char iAddr, unsigned char *pData, int iLen);
|
||||
extern int bbepI2CReadRegister(unsigned char iAddr, unsigned char u8Register, unsigned char *pData, int iLen);
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
#if defined(T5_S3_EPAPER_PRO_V2)
|
||||
// FastEPD default V2 power callback blocks forever waiting for PWRGOOD.
|
||||
// Replace it with a timeout-safe version so boot never deadlocks.
|
||||
int safeEPDiyV7EinkPower(void *pBBEP, int bOn)
|
||||
{
|
||||
static bool warnedPgood = false;
|
||||
static bool warnedTpsPg = false;
|
||||
static bool warnedTpsWrite = false;
|
||||
|
||||
FASTEPDSTATE *pState = static_cast<FASTEPDSTATE *>(pBBEP);
|
||||
if (!pState) {
|
||||
return BBEP_ERROR_BAD_PARAMETER;
|
||||
}
|
||||
|
||||
if (bOn == pState->pwr_on) {
|
||||
return BBEP_SUCCESS;
|
||||
}
|
||||
|
||||
if (bOn) {
|
||||
bbepPCA9535DigitalWrite(8, 1); // OE on
|
||||
bbepPCA9535DigitalWrite(9, 1); // GMOD on
|
||||
bbepPCA9535DigitalWrite(13, 1); // WAKEUP on
|
||||
bbepPCA9535DigitalWrite(11, 1); // PWRUP on
|
||||
bbepPCA9535DigitalWrite(12, 1); // VCOM CTRL on
|
||||
delay(1);
|
||||
|
||||
const uint32_t pgoodStart = millis();
|
||||
bool pgoodSeen = false;
|
||||
while (!bbepPCA9535DigitalRead(14)) { // CFG_PIN_PWRGOOD
|
||||
if ((millis() - pgoodStart) > 1200) {
|
||||
if (!warnedPgood) {
|
||||
LOG_WARN("ED047TC1: PWRGOOD timeout, continuing with fallback power-on path");
|
||||
warnedPgood = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
delay(1);
|
||||
}
|
||||
if (bbepPCA9535DigitalRead(14)) {
|
||||
pgoodSeen = true;
|
||||
}
|
||||
|
||||
uint8_t ucTemp[4] = {0};
|
||||
ucTemp[0] = 0x01; // TPS_REG_ENABLE
|
||||
ucTemp[1] = 0x3f; // enable rails
|
||||
const int tpsEnableRc = bbepI2CWrite(0x68, ucTemp, 2);
|
||||
|
||||
const int vcom = pState->iVCOM / -10;
|
||||
ucTemp[0] = 3; // VCOM registers 3+4 (L + H)
|
||||
ucTemp[1] = static_cast<uint8_t>(vcom);
|
||||
ucTemp[2] = static_cast<uint8_t>(vcom >> 8);
|
||||
const int tpsVcomRc = bbepI2CWrite(0x68, ucTemp, 3);
|
||||
if ((tpsEnableRc == 0 || tpsVcomRc == 0) && !warnedTpsWrite) {
|
||||
LOG_WARN("ED047TC1: TPS write did not ACK, continuing with fallback");
|
||||
warnedTpsWrite = true;
|
||||
}
|
||||
|
||||
int iTimeout = 0;
|
||||
uint8_t u8Value = 0;
|
||||
while (iTimeout < 220 && ((u8Value & 0xfa) != 0xfa)) {
|
||||
bbepI2CReadRegister(0x68, 0x0F, &u8Value, 1); // TPS_REG_PG
|
||||
iTimeout++;
|
||||
delay(1);
|
||||
}
|
||||
if (iTimeout >= 220 && !warnedTpsPg) {
|
||||
if (pgoodSeen) {
|
||||
LOG_WARN("ED047TC1: TPS power-good register timeout, panel may still work");
|
||||
} else {
|
||||
LOG_WARN("ED047TC1: TPS power-good register timeout after PWRGOOD fallback");
|
||||
}
|
||||
warnedTpsPg = true;
|
||||
}
|
||||
|
||||
pState->pwr_on = 1;
|
||||
} else {
|
||||
bbepPCA9535DigitalWrite(8, 0); // OE off
|
||||
bbepPCA9535DigitalWrite(9, 0); // GMOD off
|
||||
bbepPCA9535DigitalWrite(11, 0); // PWRUP off
|
||||
bbepPCA9535DigitalWrite(12, 0); // VCOM CTRL off
|
||||
delay(1);
|
||||
bbepPCA9535DigitalWrite(13, 0); // WAKEUP off
|
||||
pState->pwr_on = 0;
|
||||
}
|
||||
|
||||
return BBEP_SUCCESS;
|
||||
}
|
||||
#endif
|
||||
|
||||
class SafeFastEPD : public FASTEPD
|
||||
{
|
||||
public:
|
||||
void installSafePowerHandler()
|
||||
{
|
||||
#if defined(T5_S3_EPAPER_PRO_V2)
|
||||
_state.pfnEinkPower = safeEPDiyV7EinkPower;
|
||||
#endif
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
void ED047TC1::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst)
|
||||
{
|
||||
// Parallel display — SPI parameters are not used
|
||||
@@ -34,24 +144,48 @@ void ED047TC1::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_
|
||||
(void)pin_busy;
|
||||
(void)pin_rst;
|
||||
|
||||
epaper = new FASTEPD;
|
||||
SafeFastEPD *safeEpaper = new SafeFastEPD;
|
||||
epaper = safeEpaper;
|
||||
|
||||
int initRc = BBEP_ERROR_BAD_PARAMETER;
|
||||
#if defined(T5_S3_EPAPER_PRO_V1)
|
||||
epaper->initPanel(BB_PANEL_LILYGO_T5PRO, 28000000);
|
||||
initRc = epaper->initPanel(BB_PANEL_LILYGO_T5PRO, 28000000);
|
||||
#elif defined(T5_S3_EPAPER_PRO_V2)
|
||||
epaper->initPanel(BB_PANEL_LILYGO_T5PRO_V2, 28000000);
|
||||
initRc = epaper->initPanel(BB_PANEL_LILYGO_T5PRO_V2, 28000000);
|
||||
// Initialize all PCA9535 port-0 pins as outputs / HIGH
|
||||
for (int i = 0; i < 8; i++) {
|
||||
epaper->ioPinMode(i, OUTPUT);
|
||||
epaper->ioWrite(i, HIGH);
|
||||
}
|
||||
// On this board, the physical side key is labeled IO48; electrically it maps to PCA9535 IO12 (bit 2 on port-1).
|
||||
// FastEPD's generic V7 init drives 8..13 as outputs; force IO12 back to input
|
||||
// so variant touch-control polling can read the key reliably.
|
||||
epaper->ioPinMode(10, INPUT);
|
||||
#else
|
||||
#error "ED047TC1 driver: unsupported variant — define T5_S3_EPAPER_PRO_V1 or T5_S3_EPAPER_PRO_V2"
|
||||
#endif
|
||||
|
||||
epaper->setMode(BB_MODE_1BPP);
|
||||
epaper->clearWhite();
|
||||
epaper->fullUpdate(true); // Blocking initial clear
|
||||
if (initRc != BBEP_SUCCESS) {
|
||||
LOG_ERROR("ED047TC1 initPanel failed rc=%d", initRc);
|
||||
return;
|
||||
}
|
||||
|
||||
safeEpaper->installSafePowerHandler();
|
||||
|
||||
const int modeRc = epaper->setMode(BB_MODE_1BPP);
|
||||
if (modeRc != BBEP_SUCCESS) {
|
||||
LOG_WARN("ED047TC1 setMode failed rc=%d", modeRc);
|
||||
}
|
||||
|
||||
const int clearRc = epaper->clearWhite();
|
||||
if (clearRc != BBEP_SUCCESS) {
|
||||
LOG_WARN("ED047TC1 clearWhite failed rc=%d", clearRc);
|
||||
}
|
||||
|
||||
const int fullRc = epaper->fullUpdate(true); // Blocking initial clear
|
||||
if (fullRc != BBEP_SUCCESS) {
|
||||
LOG_WARN("ED047TC1 initial fullUpdate failed rc=%d", fullRc);
|
||||
}
|
||||
}
|
||||
|
||||
void ED047TC1::update(uint8_t *imageData, UpdateTypes type)
|
||||
@@ -111,9 +245,8 @@ void ED047TC1::update(uint8_t *imageData, UpdateTypes type)
|
||||
epaper->fullUpdate(CLEAR_SLOW, false);
|
||||
epaper->backupPlane(); // Sync pPrevious so next partialUpdate has a correct baseline
|
||||
} else {
|
||||
// FAST: true partial update — compares pCurrent vs pPrevious and only applies
|
||||
// the update waveform to rows that actually changed. Unchanged rows get a neutral
|
||||
// signal (no visible effect). partialUpdate() updates pPrevious internally.
|
||||
// FAST: true partial update - compares pCurrent vs pPrevious and only applies
|
||||
// update waveform to rows that changed. partialUpdate() updates pPrevious.
|
||||
epaper->partialUpdate(false, 0, dstTotalRows - 1);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user