Compare commits
122
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abdb2e07c7 | ||
|
|
5c1b6b2a23 | ||
|
|
30f9c8cf2e | ||
|
|
dc50b7fbf5 | ||
|
|
de345939af | ||
|
|
ef51c7ec11 | ||
|
|
3e873c51b7 | ||
|
|
f86cb7781e | ||
|
|
fe23dcfa3a | ||
|
|
2d6f2ba1a4 | ||
|
|
5d55353939 | ||
|
|
c3a46d4d85 | ||
|
|
0e0e17928b | ||
|
|
a522973fe7 | ||
|
|
cbfa8d7ffd | ||
|
|
782356b325 | ||
|
|
94619389bb | ||
|
|
0fdfcfce0a | ||
|
|
948c867db5 | ||
|
|
4bc1078cfb | ||
|
|
9af3d74023 | ||
|
|
23065a145e | ||
|
|
05e6a7e5eb | ||
|
|
c360815c0e | ||
|
|
f0a30f72ff | ||
|
|
20eaefe706 | ||
|
|
9a63eb729d | ||
|
|
8b8d8938b9 | ||
|
|
0bb89644f9 | ||
|
|
aef13bef89 | ||
|
|
6d31915e57 | ||
|
|
aa89faa619 | ||
|
|
ee796c75c9 | ||
|
|
918577b5b5 | ||
|
|
c5a477f4c1 | ||
|
|
fc79cf3971 | ||
|
|
0004616878 | ||
|
|
a0a7209229 | ||
|
|
16f2ff8c9e | ||
|
|
894a2f43bd | ||
|
|
e470723811 | ||
|
|
13ee648e3d | ||
|
|
bec00b0a57 | ||
|
|
025630937c | ||
|
|
527e2dc7f7 | ||
|
|
095838f375 | ||
|
|
e66fcde7e3 | ||
|
|
dc478d2163 | ||
|
|
f85ed3e59e | ||
|
|
acb7ac6e3d | ||
|
|
91314c40f5 | ||
|
|
c6282d3a3c | ||
|
|
3f35f2c6d7 | ||
|
|
407ab7abda | ||
|
|
6847f232a7 | ||
|
|
e9cd77e758 | ||
|
|
f586824435 | ||
|
|
b0d7440bad | ||
|
|
8dc82c45b8 | ||
|
|
96538b492a | ||
|
|
023461cd92 | ||
|
|
41df7ef0bf | ||
|
|
e49dab1f08 | ||
|
|
74f7d9ce3c | ||
|
|
6c74052ec3 | ||
|
|
fb7d34afb9 | ||
|
|
814773f50e | ||
|
|
621aeb29b5 | ||
|
|
6f1d611c34 | ||
|
|
fc871f42e4 | ||
|
|
9da0ee9c51 | ||
|
|
76734f0067 | ||
|
|
fd65de142b | ||
|
|
5ae50b607d | ||
|
|
20fb1b2fc2 | ||
|
|
a30ee4748a | ||
|
|
7726863043 | ||
|
|
61bab08d9e | ||
|
|
da2cd98d98 | ||
|
|
979a1a1554 | ||
|
|
a9a655c8f3 | ||
|
|
e85ebc6859 | ||
|
|
c9fc1edb4f | ||
|
|
483bb33749 | ||
|
|
62f62b4e8c | ||
|
|
ad568021ba | ||
|
|
ff8102e753 | ||
|
|
3c7d5435fe | ||
|
|
dd1184f7af | ||
|
|
46c1230c39 | ||
|
|
fa8f0f0dd8 | ||
|
|
e22a22b325 | ||
|
|
35e4a877ee | ||
|
|
a6f6175535 | ||
|
|
3e5f95f8ed | ||
|
|
ca15aa86f2 | ||
|
|
a3dcad08d4 | ||
|
|
70caa76fa0 | ||
|
|
edb975d886 | ||
|
|
8d649e9a0b | ||
|
|
5c1e5f3dd6 | ||
|
|
f1ffd05a55 | ||
|
|
6bde1d5fbb | ||
|
|
bc311616a3 | ||
|
|
9fdeec173e | ||
|
|
b24a6676a6 | ||
|
|
a3f39de4b9 | ||
|
|
d235d3f933 | ||
|
|
67726f9e3a | ||
|
|
3cbc9c91cb | ||
|
|
5c2afbf8ce | ||
|
|
f1ca363efe | ||
|
|
814dc2db1b | ||
|
|
fbeabe29ed | ||
|
|
7235afec2f | ||
|
|
ef7036e9ed | ||
|
|
c997e3bb65 | ||
|
|
028f781ea5 | ||
|
|
86cdff463b | ||
|
|
778090a269 | ||
|
|
14e9cb0fc3 | ||
|
|
aa506ce4ab |
@@ -18,7 +18,7 @@ ENV PIP_ROOT_USER_ACTION=ignore
|
||||
# trunk-ignore(hadolint/DL3008): apt packages are not pinned.
|
||||
# trunk-ignore(terrascan/AC_DOCKER_0002): apt packages are not pinned.
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||
cmake git zip libgpiod-dev libbluetooth-dev libi2c-dev \
|
||||
cmake git zip libgpiod-dev libjsoncpp-dev libbluetooth-dev libi2c-dev \
|
||||
libunistring-dev libmicrohttpd-dev libgnutls28-dev libgcrypt20-dev \
|
||||
libusb-1.0-0-dev libssl-dev pkg-config libsqlite3-dev libsdl2-dev && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/* && \
|
||||
|
||||
@@ -31,7 +31,7 @@ cmake --install "$WORK/ulfius/$SANITIZER" --prefix /usr
|
||||
cd "$SRC/firmware"
|
||||
|
||||
PLATFORMIO_EXTRA_SCRIPTS=$(echo -e "pre:.clusterfuzzlite/platformio-clusterfuzzlite-pre.py\npost:.clusterfuzzlite/platformio-clusterfuzzlite-post.py")
|
||||
STATIC_LIBS=$(pkg-config --libs --static libulfius openssl libgpiod yaml-cpp bluez --silence-errors)
|
||||
STATIC_LIBS=$(pkg-config --libs --static libulfius openssl libgpiod yaml-cpp jsoncpp bluez --silence-errors)
|
||||
export PLATFORMIO_EXTRA_SCRIPTS
|
||||
export STATIC_LIBS
|
||||
export PLATFORMIO_WORKSPACE_DIR="$WORK/pio/$SANITIZER"
|
||||
|
||||
@@ -16,6 +16,7 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
libssl-dev \
|
||||
libulfius-dev \
|
||||
libyaml-cpp-dev \
|
||||
libjsoncpp-dev \
|
||||
pipx \
|
||||
pkg-config \
|
||||
python3 \
|
||||
|
||||
@@ -13,7 +13,7 @@ runs:
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get -y update --fix-missing
|
||||
sudo apt-get install -y cppcheck libbluetooth-dev libgpiod-dev libyaml-cpp-dev lsb-release
|
||||
sudo apt-get install -y cppcheck libbluetooth-dev libgpiod-dev libyaml-cpp-dev libjsoncpp-dev lsb-release
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
|
||||
@@ -11,4 +11,4 @@ runs:
|
||||
- name: Install libs needed for native build
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get install -y libbluetooth-dev libgpiod-dev libyaml-cpp-dev openssl libssl-dev libulfius-dev liborcania-dev libusb-1.0-0-dev libi2c-dev libuv1-dev
|
||||
sudo apt-get install -y libbluetooth-dev libgpiod-dev libyaml-cpp-dev libjsoncpp-dev openssl libssl-dev libulfius-dev liborcania-dev libusb-1.0-0-dev libi2c-dev libuv1-dev
|
||||
|
||||
@@ -609,20 +609,35 @@ Most workflows can be triggered manually via `workflow_dispatch` for testing.
|
||||
|
||||
### Native unit tests (C++)
|
||||
|
||||
Unit tests in `test/` directory with 12 test suites:
|
||||
Unit tests in `test/` directory with 17 test suites:
|
||||
|
||||
- `test_crypto/` - Cryptography
|
||||
- `test_mqtt/` - MQTT integration
|
||||
- `test_radio/` - Radio interface
|
||||
- `test_mesh_module/` - Module framework
|
||||
- `test_meshpacket_serializer/` - Packet serialization
|
||||
- `test_transmit_history/` - Retransmission tracking
|
||||
- `test_admin_radio/` - LoRa region/config validation and AdminModule dispatch
|
||||
- `test_atak/` - ATAK integration
|
||||
- `test_crypto/` - Cryptography
|
||||
- `test_default/` - Default configuration
|
||||
- `test_http_content_handler/` - HTTP handling
|
||||
- `test_mac_from_string/` - MAC address parsing
|
||||
- `test_mesh_module/` - Module framework
|
||||
- `test_meshpacket_serializer/` - Packet serialization
|
||||
- `test_mqtt/` - MQTT integration
|
||||
- `test_packet_history/` - Packet history tracking
|
||||
- `test_position_precision/` - Position precision helpers
|
||||
- `test_radio/` - Radio interface
|
||||
- `test_serial/` - Serial communication
|
||||
- `test_traffic_management/` - Traffic management
|
||||
- `test_transmit_history/` - Retransmission tracking
|
||||
- `test_type_conversions/` - NodeDB v25 type conversion (bitfield round-trips, NodeInfoLite)
|
||||
- `test_utf8/` - UTF-8 utilities
|
||||
|
||||
Run with: `pio test -e native`
|
||||
Run command (preferred — avoids pipe-buffering and the Ubuntu externally-managed-environment error):
|
||||
|
||||
```bash
|
||||
~/.platformio/penv/bin/python -m platformio test -e native -f test_your_suite > /tmp/test_out.txt 2>&1
|
||||
grep -E 'error:|PASS|FAIL|succeeded|failed' /tmp/test_out.txt
|
||||
tail -15 /tmp/test_out.txt
|
||||
```
|
||||
|
||||
Do **not** use `pio test … | tail -N` — it discards build errors and shows stale cached results. Do **not** use `pio test … | grep` — line-buffering makes the terminal appear hung until the process exits. Redirect to a file first, then grep.
|
||||
|
||||
Simulation testing: `bin/test-simulator.sh`
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
name: Post Firmware Size Comment
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [CI]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
post-size-comment:
|
||||
if: >
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.conclusion != 'cancelled' &&
|
||||
github.repository == 'meshtastic/firmware'
|
||||
continue-on-error: true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download size report
|
||||
id: download
|
||||
uses: actions/download-artifact@v8
|
||||
continue-on-error: true
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
name: size-report
|
||||
path: ./
|
||||
|
||||
- name: Post or update PR comment
|
||||
if: steps.download.outcome == 'success'
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const marker = '<!-- firmware-size-report -->';
|
||||
const body = fs.readFileSync('./size-report.md', 'utf8');
|
||||
const prNumber = parseInt(fs.readFileSync('./pr-number.txt', 'utf8').trim(), 10);
|
||||
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
});
|
||||
|
||||
const existing = comments.find(c => c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body,
|
||||
});
|
||||
}
|
||||
@@ -245,47 +245,126 @@ jobs:
|
||||
path: ./*.elf
|
||||
retention-days: 30
|
||||
|
||||
shame:
|
||||
firmware-size-report:
|
||||
if: github.repository == 'meshtastic/firmware'
|
||||
continue-on-error: true
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
if: github.event_name == 'pull_request'
|
||||
with:
|
||||
filter: blob:none # means we download all the git history but none of the commit (except ones with checkout like the head)
|
||||
fetch-depth: 0
|
||||
- name: Download the current manifests
|
||||
|
||||
- name: Download current manifests
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: ./manifests-new/
|
||||
path: ./manifests/
|
||||
pattern: manifest-*
|
||||
merge-multiple: true
|
||||
- name: Upload combined manifests for later commit and global stats crunching.
|
||||
|
||||
- name: Collect current firmware sizes
|
||||
run: python3 bin/collect_sizes.py ./manifests/ ./current-sizes.json
|
||||
|
||||
- name: Upload size report artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
id: upload-manifest
|
||||
with:
|
||||
name: manifests-${{ github.sha }}
|
||||
name: firmware-sizes-${{ github.sha }}
|
||||
overwrite: true
|
||||
path: manifests-new/*.mt.json
|
||||
- name: Find the merge base
|
||||
path: ./current-sizes.json
|
||||
retention-days: 90
|
||||
|
||||
- name: Download baseline sizes from develop
|
||||
if: github.event_name == 'pull_request'
|
||||
run: echo "MERGE_BASE=$(git merge-base "origin/$base" "$head")" >> $GITHUB_ENV
|
||||
continue-on-error: true
|
||||
id: baseline-develop
|
||||
env:
|
||||
base: ${{ github.base_ref }}
|
||||
head: ${{ github.sha }}
|
||||
# Currently broken (for-loop through EVERY artifact -- rate limiting)
|
||||
# - name: Download the old manifests
|
||||
# if: github.event_name == 'pull_request'
|
||||
# run: gh run download -R "$repo" --name "manifests-$merge_base" --dir manifest-old/
|
||||
# env:
|
||||
# GH_TOKEN: ${{ github.token }}
|
||||
# merge_base: ${{ env.MERGE_BASE }}
|
||||
# repo: ${{ github.repository }}
|
||||
# - name: Do scan and post comment
|
||||
# if: github.event_name == 'pull_request'
|
||||
# run: python3 bin/shame.py ${{ github.event.pull_request.number }} manifests-old/ manifests-new/
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
RUN_ID=$(gh run list -R "${{ github.repository }}" \
|
||||
--workflow CI --branch develop --status success \
|
||||
--limit 1 --json databaseId --jq '.[0].databaseId // empty')
|
||||
if [ -n "$RUN_ID" ]; then
|
||||
ARTIFACT_NAME=$(gh api "repos/${{ github.repository }}/actions/runs/${RUN_ID}/artifacts" \
|
||||
--jq '.artifacts[] | select(.name | startswith("firmware-sizes-")) | .name' | head -1)
|
||||
if [ -n "$ARTIFACT_NAME" ]; then
|
||||
gh run download "$RUN_ID" -R "${{ github.repository }}" \
|
||||
--name "$ARTIFACT_NAME" --dir ./baseline-develop/
|
||||
cp "./baseline-develop/${ARTIFACT_NAME}/current-sizes.json" ./develop-sizes.json
|
||||
echo "found=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "found=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
else
|
||||
echo "found=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Download baseline sizes from master
|
||||
if: github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
id: baseline-master
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
RUN_ID=$(gh run list -R "${{ github.repository }}" \
|
||||
--workflow CI --branch master --status success \
|
||||
--limit 1 --json databaseId --jq '.[0].databaseId // empty')
|
||||
if [ -n "$RUN_ID" ]; then
|
||||
ARTIFACT_NAME=$(gh api "repos/${{ github.repository }}/actions/runs/${RUN_ID}/artifacts" \
|
||||
--jq '.artifacts[] | select(.name | startswith("firmware-sizes-")) | .name' | head -1)
|
||||
if [ -n "$ARTIFACT_NAME" ]; then
|
||||
gh run download "$RUN_ID" -R "${{ github.repository }}" \
|
||||
--name "$ARTIFACT_NAME" --dir ./baseline-master/
|
||||
cp "./baseline-master/${ARTIFACT_NAME}/current-sizes.json" ./master-sizes.json
|
||||
echo "found=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "found=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
else
|
||||
echo "found=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Generate size comparison report
|
||||
if: github.event_name == 'pull_request'
|
||||
id: report
|
||||
run: |
|
||||
ARGS="./current-sizes.json"
|
||||
if [ -f ./develop-sizes.json ]; then
|
||||
ARGS="$ARGS --baseline develop:./develop-sizes.json"
|
||||
fi
|
||||
if [ -f ./master-sizes.json ]; then
|
||||
ARGS="$ARGS --baseline master:./master-sizes.json"
|
||||
fi
|
||||
REPORT=$(python3 bin/size_report.py $ARGS)
|
||||
if [ -z "$REPORT" ]; then
|
||||
echo "has_report=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "has_report=true" >> "$GITHUB_OUTPUT"
|
||||
{
|
||||
echo '<!-- firmware-size-report -->'
|
||||
echo '# Firmware Size Report'
|
||||
echo ''
|
||||
echo "$REPORT"
|
||||
echo ''
|
||||
echo '---'
|
||||
echo "*Updated for ${{ github.sha }}*"
|
||||
} > ./size-report.md
|
||||
cat ./size-report.md >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
- name: Save PR number
|
||||
if: github.event_name == 'pull_request' && steps.report.outputs.has_report == 'true'
|
||||
run: echo "${{ github.event.pull_request.number }}" > ./pr-number.txt
|
||||
|
||||
- name: Upload size report
|
||||
if: github.event_name == 'pull_request' && steps.report.outputs.has_report == 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: size-report
|
||||
path: |
|
||||
./size-report.md
|
||||
./pr-number.txt
|
||||
retention-days: 5
|
||||
|
||||
release-artifacts:
|
||||
permissions: # Needed for 'gh release upload'.
|
||||
|
||||
@@ -10,18 +10,18 @@ This file (`AGENTS.md`) is a short pointer + quick reference for agents that don
|
||||
|
||||
## Quick command reference
|
||||
|
||||
| Action | Command |
|
||||
| -------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
||||
| Build a firmware variant | `pio run -e <env>` (e.g. `pio run -e rak4631`, `pio run -e heltec-v3`) |
|
||||
| Build native macOS host binary | `pio run -e native-macos` (Homebrew prereqs + CH341 LoRa setup in `variants/native/portduino/platformio.ini`) |
|
||||
| Clean + rebuild | `pio run -e <env> -t clean && pio run -e <env>` |
|
||||
| Flash a device | `pio run -e <env> -t upload --upload-port <port>` (or use the `pio_flash` MCP tool) |
|
||||
| Run firmware unit tests (native) | `pio test -e native` |
|
||||
| Run MCP hardware tests | `./mcp-server/run-tests.sh` |
|
||||
| Live TUI test runner | `mcp-server/.venv/bin/meshtastic-mcp-test-tui` |
|
||||
| Format before commit | `trunk fmt` |
|
||||
| Regenerate protobuf bindings | `bin/regen-protos.sh` |
|
||||
| Generate CI matrix | `./bin/generate_ci_matrix.py all [--level pr]` |
|
||||
| Action | Command |
|
||||
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Build a firmware variant | `pio run -e <env>` (e.g. `pio run -e rak4631`, `pio run -e heltec-v3`) |
|
||||
| Build native macOS host binary | `pio run -e native-macos` (Homebrew prereqs + CH341 LoRa setup in `variants/native/portduino/platformio.ini`) |
|
||||
| Clean + rebuild | `pio run -e <env> -t clean && pio run -e <env>` |
|
||||
| Flash a device | `pio run -e <env> -t upload --upload-port <port>` (or use the `pio_flash` MCP tool) |
|
||||
| Run firmware unit tests (native) | `~/.platformio/penv/bin/python -m platformio test -e native > /tmp/test_out.txt 2>&1` then `grep -E 'error:\|PASS\|FAIL\|succeeded\|failed' /tmp/test_out.txt` (redirect first — piping causes line-buffering) |
|
||||
| Run MCP hardware tests | `./mcp-server/run-tests.sh` |
|
||||
| Live TUI test runner | `mcp-server/.venv/bin/meshtastic-mcp-test-tui` |
|
||||
| Format before commit | `trunk fmt` |
|
||||
| Regenerate protobuf bindings | `bin/regen-protos.sh` |
|
||||
| Generate CI matrix | `./bin/generate_ci_matrix.py all [--level pr]` |
|
||||
|
||||
## MCP server (device + test automation)
|
||||
|
||||
@@ -108,7 +108,7 @@ Sequence these; don't parallelize on the same port.
|
||||
| `src/modules/` | Feature modules; `Telemetry/Sensor/` has 50+ I2C sensor drivers |
|
||||
| `variants/` | 200+ hardware variant definitions (`variant.h` + `platformio.ini` per board) |
|
||||
| `protobufs/` | `.proto` definitions; regenerate with `bin/regen-protos.sh` |
|
||||
| `test/` | Firmware unit tests (12 suites; `pio test -e native`) |
|
||||
| `test/` | Firmware unit tests (17 suites; `pio test -e native`) |
|
||||
| `mcp-server/` | Python MCP server + pytest hardware integration tests |
|
||||
| `mcp-server/tests/` | Tiered pytest suite: `unit/`, `mesh/`, `telemetry/`, `monitor/`, `recovery/`, `ui/`, `fleet/`, `admin/`, `provisioning/` |
|
||||
| `.claude/commands/` | Claude Code slash command bodies |
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@ ENV PIP_BREAK_SYSTEM_PACKAGES=1
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||
curl wget g++ zip git ca-certificates pkg-config \
|
||||
python3-pip python3-grpc-tools \
|
||||
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
|
||||
libgpiod-dev libyaml-cpp-dev libjsoncpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
|
||||
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
|
||||
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
||||
@@ -53,7 +53,7 @@ ENV TZ=Etc/UTC
|
||||
USER root
|
||||
|
||||
RUN apt-get update && apt-get --no-install-recommends -y install \
|
||||
libc-bin libc6 libgpiod3 libyaml-cpp0.8 libi2c0 libuv1t64 libusb-1.0-0-dev \
|
||||
libc-bin libc6 libgpiod3 libyaml-cpp0.8 libjsoncpp26 libi2c0 libuv1t64 libusb-1.0-0-dev \
|
||||
liborcania2.3 libulfius2.7t64 libssl3t64 \
|
||||
libx11-6 libinput10 libxkbcommon-x11-0 libsdl2-2.0-0 \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ ENV PIP_ROOT_USER_ACTION=ignore
|
||||
# hadolint ignore=DL3008
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||
g++ git ca-certificates pkg-config \
|
||||
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
|
||||
libgpiod-dev libyaml-cpp-dev libjsoncpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
|
||||
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
|
||||
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@ ENV PIP_BREAK_SYSTEM_PACKAGES=1
|
||||
RUN apk --no-cache add \
|
||||
bash g++ libstdc++-dev linux-headers zip git ca-certificates libbsd-dev \
|
||||
py3-pip py3-grpcio-tools \
|
||||
libgpiod-dev yaml-cpp-dev bluez-dev \
|
||||
libgpiod-dev yaml-cpp-dev jsoncpp-dev bluez-dev \
|
||||
libusb-dev i2c-tools-dev libuv-dev openssl-dev pkgconf argp-standalone \
|
||||
libx11-dev libinput-dev libxkbcommon-dev sqlite-dev sdl2-dev \
|
||||
&& rm -rf /var/cache/apk/* \
|
||||
@@ -48,7 +48,7 @@ LABEL org.opencontainers.image.title="Meshtastic" \
|
||||
USER root
|
||||
|
||||
RUN apk --no-cache add \
|
||||
shadow libstdc++ libbsd libgpiod yaml-cpp libusb \
|
||||
shadow libstdc++ libbsd libgpiod yaml-cpp jsoncpp libusb \
|
||||
i2c-tools libuv libx11 libinput libxkbcommon sdl2 \
|
||||
&& rm -rf /var/cache/apk/* \
|
||||
&& mkdir -p /var/lib/meshtasticd \
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""Collect firmware binary sizes from manifest (.mt.json) files into a single report."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def collect_sizes(manifest_dir):
|
||||
"""Scan manifest_dir for .mt.json files and return {board: size_bytes} dict."""
|
||||
sizes = {}
|
||||
for fname in sorted(os.listdir(manifest_dir)):
|
||||
if not fname.endswith(".mt.json"):
|
||||
continue
|
||||
path = os.path.join(manifest_dir, fname)
|
||||
with open(path) as f:
|
||||
data = json.load(f)
|
||||
board = data.get("platformioTarget", fname.replace(".mt.json", ""))
|
||||
# Find the main firmware .bin size (largest .bin, excluding OTA/littlefs/bleota)
|
||||
bin_size = None
|
||||
for entry in data.get("files", []):
|
||||
name = entry.get("name", "")
|
||||
if name.startswith("firmware-") and name.endswith(".bin"):
|
||||
bin_size = entry["bytes"]
|
||||
break
|
||||
# Fallback: any .bin that isn't ota/littlefs/bleota
|
||||
if bin_size is None:
|
||||
for entry in data.get("files", []):
|
||||
name = entry.get("name", "")
|
||||
if name.endswith(".bin") and not any(
|
||||
x in name for x in ["littlefs", "bleota", "ota"]
|
||||
):
|
||||
bin_size = entry["bytes"]
|
||||
break
|
||||
if bin_size is not None:
|
||||
sizes[board] = bin_size
|
||||
return sizes
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 3:
|
||||
print(f"Usage: {sys.argv[0]} <manifest_dir> <output.json>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
manifest_dir = sys.argv[1]
|
||||
output_path = sys.argv[2]
|
||||
|
||||
sizes = collect_sizes(manifest_dir)
|
||||
with open(output_path, "w") as f:
|
||||
json.dump(sizes, f, indent=2, sort_keys=True)
|
||||
|
||||
print(f"Collected sizes for {len(sizes)} targets -> {output_path}")
|
||||
@@ -1,95 +0,0 @@
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
from github import Github
|
||||
|
||||
def parseFile(path):
|
||||
with open(path, "r") as f:
|
||||
data = json.loads(f)
|
||||
for file in data["files"]:
|
||||
if file["name"].endswith(".bin"):
|
||||
return file["name"], file["bytes"]
|
||||
|
||||
if len(sys.argv) != 4:
|
||||
print(f"expected usage: {sys.argv[0]} <PR number> <path to old-manifests> <path to new-manifests>")
|
||||
sys.exit(1)
|
||||
|
||||
pr_number = int(sys.argv[1])
|
||||
|
||||
token = os.getenv("GITHUB_TOKEN")
|
||||
if not token:
|
||||
raise EnvironmentError("GITHUB_TOKEN not found in environment.")
|
||||
|
||||
repo_name = os.getenv("GITHUB_REPOSITORY") # "owner/repo"
|
||||
if not repo_name:
|
||||
raise EnvironmentError("GITHUB_REPOSITORY not found in environment.")
|
||||
|
||||
oldFiles = sys.argv[2]
|
||||
old = set(os.path.join(oldFiles, f) for f in os.listdir(oldFiles) if os.path.isfile(f))
|
||||
newFiles = sys.argv[3]
|
||||
new = set(os.path.join(newFiles, f) for f in os.listdir(newFiles) if os.path.isfile(f))
|
||||
|
||||
startMarkdown = "# Target Size Changes\n\n"
|
||||
markdown = ""
|
||||
|
||||
newlyIntroduced = new - old
|
||||
if len(newlyIntroduced) > 0:
|
||||
markdown += "## Newly Introduced Targets\n\n"
|
||||
# create a table
|
||||
markdown += "| File | Size |\n"
|
||||
markdown += "| ---- | ---- |\n"
|
||||
for f in newlyIntroduced:
|
||||
name, size = parseFile(f)
|
||||
markdown += f"| `{name}` | {size}b |\n"
|
||||
|
||||
# do not log removed targets
|
||||
# PRs only run a small subset of builds, so removed targets are not meaningful
|
||||
# since they are very likely to just be not ran in PR CI
|
||||
|
||||
both = old & new
|
||||
degradations = []
|
||||
improvements = []
|
||||
for f in both:
|
||||
oldName, oldSize = parseFile(f)
|
||||
_, newSize = parseFile(f)
|
||||
if oldSize != newSize:
|
||||
if newSize < oldSize:
|
||||
improvements.append((oldName, oldSize, newSize))
|
||||
else:
|
||||
degradations.append((oldName, oldSize, newSize))
|
||||
|
||||
if len(degradations) > 0:
|
||||
markdown += "\n## Degradation\n\n"
|
||||
# create a table
|
||||
markdown += "| File | Difference | Old Size | New Size |\n"
|
||||
markdown += "| ---- | ---------- | -------- | -------- |\n"
|
||||
for oldName, oldSize, newSize in degradations:
|
||||
markdown += f"| `{oldName}` | **{oldSize - newSize}b** | {oldSize}b | {newSize}b |\n"
|
||||
|
||||
if len(improvements) > 0:
|
||||
markdown += "\n## Improvement\n\n"
|
||||
# create a table
|
||||
markdown += "| File | Difference | Old Size | New Size |\n"
|
||||
markdown += "| ---- | ---------- | -------- | -------- |\n"
|
||||
for oldName, oldSize, newSize in improvements:
|
||||
markdown += f"| `{oldName}` | **{oldSize - newSize}b** | {oldSize}b | {newSize}b |\n"
|
||||
|
||||
if len(markdown) == 0:
|
||||
markdown = "No changes in target sizes detected."
|
||||
|
||||
g = Github(token)
|
||||
repo = g.get_repo(repo_name)
|
||||
pr = repo.get_pull(pr_number)
|
||||
|
||||
existing_comment = None
|
||||
for comment in pr.get_issue_comments():
|
||||
if comment.body.startswith(startMarkdown):
|
||||
existing_comment = comment
|
||||
break
|
||||
|
||||
final_markdown = startMarkdown + markdown
|
||||
|
||||
if existing_comment:
|
||||
existing_comment.edit(body=final_markdown)
|
||||
else:
|
||||
pr.create_issue_comment(body=final_markdown)
|
||||
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""Compare firmware size reports and generate a markdown summary.
|
||||
|
||||
Usage:
|
||||
size_report.py <new_sizes.json> [--baseline <label>:<old_sizes.json>]...
|
||||
|
||||
Examples:
|
||||
# Compare PR against develop and master baselines
|
||||
size_report.py pr.json --baseline develop:develop.json --baseline master:master.json
|
||||
|
||||
# Single baseline comparison
|
||||
size_report.py pr.json --baseline develop:develop.json
|
||||
|
||||
# No baselines — shows sizes with blank delta columns
|
||||
size_report.py pr.json
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
|
||||
|
||||
def load_sizes(path):
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def format_delta(n):
|
||||
"""Format byte delta with sign and human-friendly suffix."""
|
||||
sign = "+" if n > 0 else ""
|
||||
if abs(n) >= 1024:
|
||||
return f"{sign}{n:,} ({sign}{n / 1024:.1f} KB)"
|
||||
return f"{sign}{n:,}"
|
||||
|
||||
|
||||
def generate_markdown(new_sizes, baselines, top_n=5):
|
||||
"""Generate a single table with current size and delta columns per baseline.
|
||||
|
||||
baselines: list of (label, old_sizes_dict), may be empty
|
||||
"""
|
||||
labels = [label for label, _ in baselines]
|
||||
|
||||
# Build rows: (board, current_size, [(delta, abs_delta) per baseline])
|
||||
rows = []
|
||||
for board in sorted(new_sizes):
|
||||
current = new_sizes[board]
|
||||
deltas = []
|
||||
for _, old_sizes in baselines:
|
||||
old = old_sizes.get(board)
|
||||
if old is not None:
|
||||
d = current - old
|
||||
deltas.append((d, abs(d)))
|
||||
else:
|
||||
deltas.append((None, 0))
|
||||
# Sort key: max abs delta across baselines (biggest changes first)
|
||||
max_abs = max((ad for _, ad in deltas), default=0)
|
||||
rows.append((board, current, deltas, max_abs))
|
||||
|
||||
rows.sort(key=lambda r: r[3], reverse=True)
|
||||
|
||||
# Summary line
|
||||
sections = []
|
||||
summary_parts = [f"{len(new_sizes)} targets"]
|
||||
for i, (label, old_sizes) in enumerate(baselines):
|
||||
increases = sum(
|
||||
1 for _, _, deltas, _ in rows if deltas[i][0] is not None and deltas[i][0] > 0
|
||||
)
|
||||
decreases = sum(
|
||||
1 for _, _, deltas, _ in rows if deltas[i][0] is not None and deltas[i][0] < 0
|
||||
)
|
||||
net = sum(
|
||||
deltas[i][0] for _, _, deltas, _ in rows if deltas[i][0] is not None
|
||||
)
|
||||
parts = []
|
||||
if increases:
|
||||
parts.append(f"{increases} increased")
|
||||
if decreases:
|
||||
parts.append(f"{decreases} decreased")
|
||||
if parts:
|
||||
parts.append(f"net {format_delta(net)}")
|
||||
summary_parts.append(f"vs `{label}`: {', '.join(parts)}")
|
||||
else:
|
||||
summary_parts.append(f"vs `{label}`: no changes")
|
||||
|
||||
if not baselines:
|
||||
summary_parts.append("no baseline available yet")
|
||||
|
||||
sections.append(f"**{' | '.join(summary_parts)}**\n")
|
||||
|
||||
# Table header
|
||||
header = "| Target | Size |"
|
||||
separator = "|--------|-----:|"
|
||||
for label in labels:
|
||||
header += f" vs `{label}` |"
|
||||
separator += "----------:|"
|
||||
sections.append(header)
|
||||
sections.append(separator)
|
||||
|
||||
def format_row(board, current, deltas):
|
||||
row = f"| `{board}` | {current:,} |"
|
||||
for d, _ in deltas:
|
||||
if d is None:
|
||||
row += " |"
|
||||
elif d == 0:
|
||||
row += " 0 |"
|
||||
else:
|
||||
icon = "📈" if d > 0 else "📉"
|
||||
row += f" {icon} {format_delta(d)} |"
|
||||
return row
|
||||
|
||||
# Top N rows always visible
|
||||
top = rows[:top_n]
|
||||
for board, current, deltas, _ in top:
|
||||
sections.append(format_row(board, current, deltas))
|
||||
|
||||
# Remaining rows in expandable section
|
||||
rest = rows[top_n:]
|
||||
if rest:
|
||||
sections.append("")
|
||||
sections.append(
|
||||
f"<details><summary>Show {len(rest)} more target(s)</summary>\n"
|
||||
)
|
||||
sections.append(header)
|
||||
sections.append(separator)
|
||||
for board, current, deltas, _ in rest:
|
||||
sections.append(format_row(board, current, deltas))
|
||||
sections.append("\n</details>")
|
||||
|
||||
sections.append("")
|
||||
return "\n".join(sections)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Compare firmware size reports")
|
||||
parser.add_argument("new_sizes", help="Path to new sizes JSON")
|
||||
parser.add_argument(
|
||||
"--baseline",
|
||||
action="append",
|
||||
default=[],
|
||||
metavar="LABEL:PATH",
|
||||
help="Baseline to compare against (e.g. develop:develop.json)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--top",
|
||||
type=int,
|
||||
default=5,
|
||||
help="Number of top changes to show before collapsing (default: 5)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
new_sizes = load_sizes(args.new_sizes)
|
||||
|
||||
# Silence output when no targets were built — repo maintainer choice
|
||||
if not new_sizes:
|
||||
return
|
||||
|
||||
baselines = []
|
||||
for b in args.baseline:
|
||||
if ":" not in b:
|
||||
print(f"Error: baseline must be LABEL:PATH, got '{b}'", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
label, path = b.split(":", 1)
|
||||
baselines.append((label, load_sizes(path)))
|
||||
|
||||
md = generate_markdown(new_sizes, baselines, top_n=args.top)
|
||||
print(md)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,262 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""Tests for bin/collect_sizes.py and bin/size_report.py."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
SCRIPTS_DIR = os.path.join(os.path.dirname(__file__), "..", "bin")
|
||||
|
||||
|
||||
def make_manifest(target, firmware_bytes, extra_files=None):
|
||||
"""Create a minimal .mt.json manifest dict."""
|
||||
files = [{"name": f"firmware-{target}-2.6.0.bin", "bytes": firmware_bytes}]
|
||||
if extra_files:
|
||||
files.extend(extra_files)
|
||||
return {
|
||||
"platformioTarget": target,
|
||||
"version": "2.6.0.test",
|
||||
"files": files,
|
||||
}
|
||||
|
||||
|
||||
def write_manifests(tmpdir, manifests):
|
||||
"""Write manifest dicts as .mt.json files into tmpdir."""
|
||||
for target, data in manifests.items():
|
||||
path = os.path.join(tmpdir, f"firmware-{target}.mt.json")
|
||||
with open(path, "w") as f:
|
||||
json.dump(data, f)
|
||||
|
||||
|
||||
def run_script(script, args):
|
||||
"""Run a Python script and return (returncode, stdout, stderr)."""
|
||||
result = subprocess.run(
|
||||
[sys.executable, os.path.join(SCRIPTS_DIR, script)] + args,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return result.returncode, result.stdout, result.stderr
|
||||
|
||||
|
||||
def test_collect_sizes_basic():
|
||||
"""collect_sizes picks up firmware-*.bin entries from manifests."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
outfile = os.path.join(tmpdir, "sizes.json")
|
||||
manifests = {
|
||||
"heltec-v3": make_manifest("heltec-v3", 1048576),
|
||||
"rak4631": make_manifest("rak4631", 524288),
|
||||
"tbeam": make_manifest("tbeam", 786432),
|
||||
}
|
||||
write_manifests(tmpdir, manifests)
|
||||
|
||||
rc, stdout, stderr = run_script("collect_sizes.py", [tmpdir, outfile])
|
||||
assert rc == 0, f"collect_sizes failed: {stderr}"
|
||||
assert "3 targets" in stdout
|
||||
|
||||
with open(outfile) as f:
|
||||
sizes = json.load(f)
|
||||
assert sizes == {"heltec-v3": 1048576, "rak4631": 524288, "tbeam": 786432}
|
||||
|
||||
|
||||
def test_collect_sizes_fallback_bin():
|
||||
"""collect_sizes falls back to non-firmware-prefixed .bin if no firmware-*.bin."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
outfile = os.path.join(tmpdir, "sizes.json")
|
||||
# Manifest with only a generic .bin (no firmware- prefix)
|
||||
data = {
|
||||
"platformioTarget": "custom-board",
|
||||
"files": [
|
||||
{"name": "littlefs-custom-board.bin", "bytes": 100000},
|
||||
{"name": "custom-board.bin", "bytes": 500000},
|
||||
],
|
||||
}
|
||||
path = os.path.join(tmpdir, "firmware-custom-board.mt.json")
|
||||
with open(path, "w") as f:
|
||||
json.dump(data, f)
|
||||
|
||||
rc, stdout, stderr = run_script("collect_sizes.py", [tmpdir, outfile])
|
||||
assert rc == 0, f"collect_sizes failed: {stderr}"
|
||||
|
||||
with open(outfile) as f:
|
||||
sizes = json.load(f)
|
||||
assert sizes == {"custom-board": 500000}
|
||||
|
||||
|
||||
def test_collect_sizes_skips_ota_littlefs():
|
||||
"""collect_sizes ignores ota/littlefs/bleota .bin files in fallback."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
outfile = os.path.join(tmpdir, "sizes.json")
|
||||
data = {
|
||||
"platformioTarget": "board-x",
|
||||
"files": [
|
||||
{"name": "littlefs-board-x.bin", "bytes": 100000},
|
||||
{"name": "bleota-board-x.bin", "bytes": 50000},
|
||||
{"name": "mt-board-x-ota.bin", "bytes": 60000},
|
||||
],
|
||||
}
|
||||
path = os.path.join(tmpdir, "firmware-board-x.mt.json")
|
||||
with open(path, "w") as f:
|
||||
json.dump(data, f)
|
||||
|
||||
rc, stdout, stderr = run_script("collect_sizes.py", [tmpdir, outfile])
|
||||
assert rc == 0
|
||||
with open(outfile) as f:
|
||||
sizes = json.load(f)
|
||||
# No valid firmware .bin found, board should be absent
|
||||
assert sizes == {}
|
||||
|
||||
|
||||
def test_collect_sizes_ignores_non_mt_json():
|
||||
"""collect_sizes skips non .mt.json files."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
outfile = os.path.join(tmpdir, "sizes.json")
|
||||
# Write a valid manifest
|
||||
manifests = {"rak4631": make_manifest("rak4631", 500000)}
|
||||
write_manifests(tmpdir, manifests)
|
||||
# Write a decoy file
|
||||
with open(os.path.join(tmpdir, "readme.txt"), "w") as f:
|
||||
f.write("not a manifest")
|
||||
|
||||
rc, stdout, stderr = run_script("collect_sizes.py", [tmpdir, outfile])
|
||||
assert rc == 0
|
||||
with open(outfile) as f:
|
||||
sizes = json.load(f)
|
||||
assert list(sizes.keys()) == ["rak4631"]
|
||||
|
||||
|
||||
def test_size_report_no_baseline():
|
||||
"""size_report with no baselines shows sizes only."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
sizes_file = os.path.join(tmpdir, "new.json")
|
||||
with open(sizes_file, "w") as f:
|
||||
json.dump({"heltec-v3": 1000000, "rak4631": 500000}, f)
|
||||
|
||||
rc, stdout, stderr = run_script("size_report.py", [sizes_file])
|
||||
assert rc == 0, f"size_report failed: {stderr}"
|
||||
assert "2 targets" in stdout
|
||||
assert "no baseline available yet" in stdout
|
||||
assert "`heltec-v3`" in stdout
|
||||
assert "`rak4631`" in stdout
|
||||
|
||||
|
||||
def test_size_report_with_baseline():
|
||||
"""size_report shows deltas against a baseline."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
new_file = os.path.join(tmpdir, "new.json")
|
||||
old_file = os.path.join(tmpdir, "old.json")
|
||||
with open(new_file, "w") as f:
|
||||
json.dump({"heltec-v3": 1050000, "rak4631": 500000, "tbeam": 800000}, f)
|
||||
with open(old_file, "w") as f:
|
||||
json.dump({"heltec-v3": 1000000, "rak4631": 500000, "tbeam": 810000}, f)
|
||||
|
||||
rc, stdout, stderr = run_script(
|
||||
"size_report.py", [new_file, "--baseline", f"develop:{old_file}"]
|
||||
)
|
||||
assert rc == 0, f"size_report failed: {stderr}"
|
||||
assert "3 targets" in stdout
|
||||
assert "1 increased" in stdout
|
||||
assert "1 decreased" in stdout
|
||||
# heltec-v3 grew by 50000
|
||||
assert "📈" in stdout
|
||||
# tbeam shrank by 10000
|
||||
assert "📉" in stdout
|
||||
# rak4631 unchanged
|
||||
assert "vs `develop`" in stdout
|
||||
|
||||
|
||||
def test_size_report_multiple_baselines():
|
||||
"""size_report handles multiple baselines."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
new_file = os.path.join(tmpdir, "new.json")
|
||||
dev_file = os.path.join(tmpdir, "develop.json")
|
||||
master_file = os.path.join(tmpdir, "master.json")
|
||||
with open(new_file, "w") as f:
|
||||
json.dump({"board-a": 100000}, f)
|
||||
with open(dev_file, "w") as f:
|
||||
json.dump({"board-a": 95000}, f)
|
||||
with open(master_file, "w") as f:
|
||||
json.dump({"board-a": 90000}, f)
|
||||
|
||||
rc, stdout, stderr = run_script(
|
||||
"size_report.py",
|
||||
[new_file, "--baseline", f"develop:{dev_file}", "--baseline", f"master:{master_file}"],
|
||||
)
|
||||
assert rc == 0, f"size_report failed: {stderr}"
|
||||
assert "vs `develop`" in stdout
|
||||
assert "vs `master`" in stdout
|
||||
|
||||
|
||||
def test_size_report_new_target_no_baseline_entry():
|
||||
"""size_report handles targets not present in baseline (new boards)."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
new_file = os.path.join(tmpdir, "new.json")
|
||||
old_file = os.path.join(tmpdir, "old.json")
|
||||
with open(new_file, "w") as f:
|
||||
json.dump({"new-board": 300000, "existing": 500000}, f)
|
||||
with open(old_file, "w") as f:
|
||||
json.dump({"existing": 500000}, f)
|
||||
|
||||
rc, stdout, stderr = run_script(
|
||||
"size_report.py", [new_file, "--baseline", f"develop:{old_file}"]
|
||||
)
|
||||
assert rc == 0, f"size_report failed: {stderr}"
|
||||
assert "`new-board`" in stdout
|
||||
assert "no changes" in stdout # only existing is compared, delta=0
|
||||
|
||||
|
||||
def test_size_report_all_unchanged():
|
||||
"""size_report shows 'no changes' when all sizes match."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
sizes_file = os.path.join(tmpdir, "sizes.json")
|
||||
with open(sizes_file, "w") as f:
|
||||
json.dump({"board-a": 100000, "board-b": 200000}, f)
|
||||
|
||||
rc, stdout, stderr = run_script(
|
||||
"size_report.py", [sizes_file, "--baseline", f"develop:{sizes_file}"]
|
||||
)
|
||||
assert rc == 0, f"size_report failed: {stderr}"
|
||||
assert "no changes" in stdout
|
||||
|
||||
|
||||
def test_collect_sizes_bad_args():
|
||||
"""collect_sizes exits with error on wrong arg count."""
|
||||
rc, stdout, stderr = run_script("collect_sizes.py", [])
|
||||
assert rc == 1
|
||||
assert "Usage" in stderr
|
||||
|
||||
|
||||
def test_size_report_bad_baseline_format():
|
||||
"""size_report exits with error on malformed --baseline."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
sizes_file = os.path.join(tmpdir, "sizes.json")
|
||||
with open(sizes_file, "w") as f:
|
||||
json.dump({"x": 1}, f)
|
||||
|
||||
rc, stdout, stderr = run_script(
|
||||
"size_report.py", [sizes_file, "--baseline", "no-colon-here"]
|
||||
)
|
||||
assert rc == 1
|
||||
assert "LABEL:PATH" in stderr
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
tests = [v for k, v in globals().items() if k.startswith("test_")]
|
||||
passed = 0
|
||||
failed = 0
|
||||
for test in tests:
|
||||
try:
|
||||
test()
|
||||
print(f" PASS: {test.__name__}")
|
||||
passed += 1
|
||||
except AssertionError as e:
|
||||
print(f" FAIL: {test.__name__}: {e}")
|
||||
failed += 1
|
||||
except Exception as e:
|
||||
print(f" ERROR: {test.__name__}: {type(e).__name__}: {e}")
|
||||
failed += 1
|
||||
|
||||
print(f"\n{passed} passed, {failed} failed out of {passed + failed}")
|
||||
sys.exit(1 if failed else 0)
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"build": {
|
||||
"core": "esp32",
|
||||
"extra_flags": ["-DBOARD_HAS_PSRAM"],
|
||||
"f_cpu": "360000000L",
|
||||
"f_flash": "80000000L",
|
||||
"f_psram": "200000000L",
|
||||
"flash_mode": "qio",
|
||||
"hwids": [["0x1A86", "0x7522"]],
|
||||
"mcu": "esp32p4",
|
||||
"chip_variant": "esp32p4_es",
|
||||
"variant": "esp32p4"
|
||||
},
|
||||
"arduino": {
|
||||
"partitions": "default_16MB.csv"
|
||||
},
|
||||
"connectivity": ["bluetooth", "openthread"],
|
||||
"debug": {
|
||||
"openocd_target": "esp32p4.cfg"
|
||||
},
|
||||
"frameworks": ["arduino", "espidf"],
|
||||
"name": "CrowPanel Advanced ESP32-P4 HMI AI Display",
|
||||
"upload": {
|
||||
"flash_size": "16MB",
|
||||
"maximum_ram_size": 512000,
|
||||
"maximum_size": 16777216,
|
||||
"require_upload_port": true,
|
||||
"speed": 1500000
|
||||
},
|
||||
"url": "https://www.elecrow.com/crowpanel-advanced-5inch-esp32-p4-hmi-ai-display-800x480-ips-touch-screen-with-wifi-6.html",
|
||||
"vendor": "Elecrow"
|
||||
}
|
||||
Vendored
+1
@@ -14,6 +14,7 @@ Build-Depends: debhelper-compat (= 13),
|
||||
g++,
|
||||
pkg-config,
|
||||
libyaml-cpp-dev,
|
||||
libjsoncpp-dev,
|
||||
libgpiod-dev,
|
||||
libbluetooth-dev,
|
||||
libusb-1.0-0-dev,
|
||||
|
||||
@@ -191,10 +191,12 @@ echo
|
||||
# PASS/FAIL — every hardware test would SKIP with "role not present". We
|
||||
# narrow to tests/unit explicitly so the summary reads as "no hardware,
|
||||
# unit suite only" instead of "big skip count looks suspicious".
|
||||
# Keep terminal output condensed (`-q -r fE`) so skip-heavy runs do not print
|
||||
# each skipped test in full; skip counts still appear in pytest's summary.
|
||||
if [[ -z $DETECTED && $# -eq 0 ]]; then
|
||||
echo "[pre-flight] no supported devices detected; running unit tier only."
|
||||
echo
|
||||
exec "$VENV_PY" -m pytest tests/unit -v --report-log=tests/reportlog.jsonl
|
||||
exec "$VENV_PY" -m pytest tests/unit -q -r fE --report-log=tests/reportlog.jsonl
|
||||
fi
|
||||
|
||||
# Default pytest args when the user passed none. Power users can invoke
|
||||
@@ -210,11 +212,13 @@ fi
|
||||
# skipping half the hardware tests with "not baked with session profile"
|
||||
# errors. Power users who know their hardware is current and want to shave
|
||||
# those seconds can pass `--assume-baked` explicitly.
|
||||
# Defaults also use condensed reporting (`-q -r fE`) to avoid listing every
|
||||
# skipped test verbatim while still surfacing failures/errors and summary data.
|
||||
if [[ $# -eq 0 ]]; then
|
||||
set -- tests/ \
|
||||
--html=tests/report.html --self-contained-html \
|
||||
--junitxml=tests/junit.xml \
|
||||
-v --tb=short
|
||||
-q -r fE --tb=short
|
||||
fi
|
||||
|
||||
# UI tier requires opencv-python-headless (and ideally easyocr). If it's
|
||||
|
||||
@@ -34,6 +34,7 @@ BuildRequires: python3dist(grpcio-tools)
|
||||
BuildRequires: git-core
|
||||
BuildRequires: gcc-c++
|
||||
BuildRequires: pkgconfig(yaml-cpp)
|
||||
BuildRequires: pkgconfig(jsoncpp)
|
||||
BuildRequires: pkgconfig(libgpiod)
|
||||
BuildRequires: pkgconfig(bluez)
|
||||
BuildRequires: pkgconfig(libusb-1.0)
|
||||
|
||||
@@ -208,16 +208,10 @@ lib_deps =
|
||||
https://github.com/adafruit/Adafruit_BMP3XX/archive/refs/tags/2.1.6.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit MAX1704X packageName=adafruit/Adafruit_MAX1704X
|
||||
https://github.com/adafruit/Adafruit_MAX1704X/archive/refs/tags/1.0.3.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit SHTC3 packageName=adafruit/Adafruit_SHTC3
|
||||
https://github.com/adafruit/Adafruit_SHTC3/archive/refs/tags/1.0.2.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit LPS2X packageName=adafruit/Adafruit_LPS2X
|
||||
https://github.com/adafruit/Adafruit_LPS2X/archive/refs/tags/2.0.6.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit SHT31 packageName=adafruit/Adafruit_SHT31
|
||||
https://github.com/adafruit/Adafruit_SHT31/archive/refs/tags/2.2.2.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit VEML7700 packageName=adafruit/Adafruit_VEML7700
|
||||
https://github.com/adafruit/Adafruit_VEML7700/archive/refs/tags/2.1.6.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit SHT4x packageName=adafruit/Adafruit_SHT4X
|
||||
https://github.com/adafruit/Adafruit_SHT4X/archive/refs/tags/1.0.5.zip
|
||||
# renovate: datasource=github-tags depName=SparkFun Qwiic Scale NAU7802 packageName=sparkfun/SparkFun_Qwiic_Scale_NAU7802_Arduino_Library
|
||||
https://github.com/sparkfun/SparkFun_Qwiic_Scale_NAU7802_Arduino_Library/archive/refs/tags/v1.0.6.zip
|
||||
# renovate: datasource=custom.pio depName=ClosedCube OPT3001 packageName=closedcube/library/ClosedCube OPT3001
|
||||
|
||||
+1
-1
Submodule protobufs updated: 519a0c7c9c...8b68f27367
+25
-1
@@ -14,4 +14,28 @@ const uint8_t FROMNUM_UUID_16[16u] = {0x53, 0x44, 0xe3, 0x47, 0x75, 0xaa, 0x70,
|
||||
const uint8_t LEGACY_LOGRADIO_UUID_16[16u] = {0xe2, 0xf2, 0x1e, 0xbe, 0xc5, 0x15, 0xcf, 0xaa,
|
||||
0x6b, 0x43, 0xfa, 0x78, 0x38, 0xd2, 0x6f, 0x6c};
|
||||
const uint8_t LOGRADIO_UUID_16[16u] = {0x47, 0x95, 0xDF, 0x8C, 0xDE, 0xE9, 0x44, 0x99,
|
||||
0x23, 0x44, 0xE6, 0x06, 0x49, 0x6E, 0x3D, 0x5A};
|
||||
0x23, 0x44, 0xE6, 0x06, 0x49, 0x6E, 0x3D, 0x5A};
|
||||
|
||||
void BluetoothApi::setup() {}
|
||||
void BluetoothApi::shutdown() {}
|
||||
void BluetoothApi::deinit() {}
|
||||
void BluetoothApi::clearBonds() {}
|
||||
bool BluetoothApi::isActive()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
bool BluetoothApi::isConnected()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
void BluetoothApi::sendLog(const uint8_t *logMessage, size_t length)
|
||||
{
|
||||
(void)logMessage;
|
||||
(void)length;
|
||||
}
|
||||
|
||||
void updateBatteryLevel(uint8_t level) __attribute__((weak));
|
||||
void updateBatteryLevel(uint8_t level)
|
||||
{
|
||||
(void)level;
|
||||
}
|
||||
@@ -24,9 +24,14 @@ void updateBatteryLevel(uint8_t level);
|
||||
class BluetoothApi
|
||||
{
|
||||
public:
|
||||
virtual ~BluetoothApi() = default;
|
||||
|
||||
virtual void setup();
|
||||
virtual void shutdown();
|
||||
virtual void deinit();
|
||||
virtual void clearBonds();
|
||||
virtual bool isActive();
|
||||
virtual bool isConnected();
|
||||
virtual int getRssi() = 0;
|
||||
virtual void sendLog(const uint8_t *logMessage, size_t length);
|
||||
};
|
||||
@@ -51,6 +51,12 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC
|
||||
case PRESET(NARROW_SLOW):
|
||||
return useShortName ? "NarS" : "NarrowSlow";
|
||||
break;
|
||||
case PRESET(TINY_FAST):
|
||||
return useShortName ? "TinyF" : "TinyFast";
|
||||
break;
|
||||
case PRESET(TINY_SLOW):
|
||||
return useShortName ? "TinyS" : "TinySlow";
|
||||
break;
|
||||
default:
|
||||
return useShortName ? "Custom" : "Invalid";
|
||||
break;
|
||||
|
||||
+11
-1
@@ -1,6 +1,16 @@
|
||||
// TODO refactor this out with better radio configuration system
|
||||
#ifdef USE_RF95
|
||||
|
||||
#ifndef RF95_RESET
|
||||
#define RF95_RESET LORA_RESET
|
||||
#define RF95_IRQ LORA_DIO0 // on SX1262 version this is a no connect DIO0
|
||||
#endif
|
||||
|
||||
#ifndef RF95_IRQ
|
||||
#define RF95_IRQ LORA_DIO0 // on SX1262 version this is a no connect DIO0
|
||||
#endif
|
||||
|
||||
#ifndef RF95_DIO1
|
||||
#define RF95_DIO1 LORA_DIO1 // Note: not really used for RF95, but used for pure SX127x
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
@@ -51,7 +51,7 @@ size_t RedirectablePrint::write(uint8_t c)
|
||||
size_t RedirectablePrint::vprintf(const char *logLevel, const char *format, va_list arg)
|
||||
{
|
||||
va_list copy;
|
||||
#if ENABLE_JSON_LOGGING || ARCH_PORTDUINO
|
||||
#if ARCH_PORTDUINO
|
||||
static char printBuf[512];
|
||||
#else
|
||||
static char printBuf[160];
|
||||
@@ -222,7 +222,7 @@ void RedirectablePrint::log_to_ble(const char *logLevel, const char *format, va_
|
||||
if (config.security.debug_log_api_enabled && !pauseBluetoothLogging) {
|
||||
bool isBleConnected = false;
|
||||
#ifdef ARCH_ESP32
|
||||
isBleConnected = nimbleBluetooth && nimbleBluetooth->isActive() && nimbleBluetooth->isConnected();
|
||||
isBleConnected = bluetoothApi && bluetoothApi->isActive() && bluetoothApi->isConnected();
|
||||
#elif defined(ARCH_NRF52)
|
||||
isBleConnected = nrf52Bluetooth != nullptr && nrf52Bluetooth->isConnected();
|
||||
#elif defined(ARCH_NRF54L15)
|
||||
@@ -240,7 +240,7 @@ void RedirectablePrint::log_to_ble(const char *logLevel, const char *format, va_
|
||||
auto buffer = std::unique_ptr<uint8_t[]>(new uint8_t[meshtastic_LogRecord_size]);
|
||||
size_t size = pb_encode_to_bytes(buffer.get(), meshtastic_LogRecord_size, meshtastic_LogRecord_fields, &logRecord);
|
||||
#ifdef ARCH_ESP32
|
||||
nimbleBluetooth->sendLog(buffer.get(), size);
|
||||
bluetoothApi->sendLog(buffer.get(), size);
|
||||
#elif defined(ARCH_NRF52)
|
||||
nrf52Bluetooth->sendLog(buffer.get(), size);
|
||||
#elif defined(ARCH_NRF54L15)
|
||||
|
||||
@@ -0,0 +1,762 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32P4) && defined(CONFIG_ESP_HOSTED_ENABLED) && !MESHTASTIC_EXCLUDE_BLUETOOTH
|
||||
#include "BluetoothStatus.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "bluetooth/HostedBluetooth.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "esp32-hal-hosted.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_event.h"
|
||||
#include "esp_hosted.h"
|
||||
#include "esp_hosted_event.h"
|
||||
#include "main.h"
|
||||
#include "mesh/PhoneAPI.h"
|
||||
#include "mesh/mesh-pb-constants.h"
|
||||
#include <BLEAdvertising.h>
|
||||
#include <BLEDevice.h>
|
||||
#include <BLESecurity.h>
|
||||
#include <BLEServer.h>
|
||||
#include <BLEUtils.h>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <climits>
|
||||
#include <cstring>
|
||||
#include <driver/gpio.h>
|
||||
#include <mutex>
|
||||
|
||||
#include "host/ble_gap.h"
|
||||
#include "host/ble_store.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
/*
|
||||
* Maintainer note: HostedBluetooth intentionally stays close to NimbleBluetooth
|
||||
* but is not a strict drop-in equivalent.
|
||||
*
|
||||
* Intentional differences from NimbleBluetooth include:
|
||||
* - ESP-Hosted transport lifecycle handling (event callbacks + CP reset GPIO control).
|
||||
* - Data-length update behavior (Hosted currently requests ble_gap_set_data_len on connect).
|
||||
* - No Battery Service exposure here.
|
||||
*
|
||||
* If you modify common BLE flow in one class, review and likely mirror in both:
|
||||
* - PhoneAPI queueing/synchronization between BLE callbacks and runOnce().
|
||||
* - Security/pairing config and passkey UX/status updates.
|
||||
* - Mesh GATT characteristics, permissions, and advertising setup.
|
||||
* - Connection parameter tuning and reconnect/disconnect cleanup paths.
|
||||
*/
|
||||
constexpr uint16_t kPreferredBleMtu = 517;
|
||||
constexpr uint16_t kPreferredBleTxOctets = 251;
|
||||
constexpr uint16_t kPreferredBleTxTimeUs = (kPreferredBleTxOctets + 14) * 8;
|
||||
constexpr size_t kBluetoothToPhoneQueueSize = 3;
|
||||
constexpr size_t kBluetoothFromPhoneQueueSize = 3;
|
||||
|
||||
BLECharacteristic *fromNumCharacteristic = nullptr;
|
||||
BLECharacteristic *logRadioCharacteristic = nullptr;
|
||||
BLEServer *bleServer = nullptr;
|
||||
|
||||
static bool passkeyShowing = false;
|
||||
std::atomic<uint16_t> hostedConnHandle{BLE_HS_CONN_HANDLE_NONE};
|
||||
|
||||
void clearPairingDisplay()
|
||||
{
|
||||
if (!passkeyShowing) {
|
||||
return;
|
||||
}
|
||||
|
||||
passkeyShowing = false;
|
||||
#if HAS_SCREEN
|
||||
if (screen) {
|
||||
screen->endAlert();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
class HostedBluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
HostedBluetoothPhoneAPI() : concurrency::OSThread("HostedBluetooth") { api_type = TYPE_BLE; }
|
||||
|
||||
std::mutex fromPhoneMutex;
|
||||
std::atomic<size_t> fromPhoneQueueSize{0};
|
||||
std::array<BLEValue, kBluetoothFromPhoneQueueSize> fromPhoneQueue{};
|
||||
|
||||
std::mutex toPhoneMutex;
|
||||
std::atomic<size_t> toPhoneQueueSize{0};
|
||||
std::array<std::array<uint8_t, meshtastic_FromRadio_size>, kBluetoothToPhoneQueueSize> toPhoneQueue{};
|
||||
std::array<size_t, kBluetoothToPhoneQueueSize> toPhoneQueueByteSizes{};
|
||||
std::atomic<bool> onReadCallbackIsWaitingForData{false};
|
||||
|
||||
protected:
|
||||
int32_t runOnce() override
|
||||
{
|
||||
while (fromPhoneQueueSize > 0 || onReadCallbackIsWaitingForData) {
|
||||
runOnceHandleFromPhoneQueue();
|
||||
runOnceHandleToPhoneQueue();
|
||||
}
|
||||
return INT32_MAX;
|
||||
}
|
||||
|
||||
void onNowHasData(uint32_t fromRadioNum) override
|
||||
{
|
||||
PhoneAPI::onNowHasData(fromRadioNum);
|
||||
|
||||
if (!fromNumCharacteristic || !bleServer || bleServer->getConnectedCount() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t val[4];
|
||||
put_le32(val, fromRadioNum);
|
||||
fromNumCharacteristic->setValue(val, sizeof(val));
|
||||
fromNumCharacteristic->notify();
|
||||
}
|
||||
|
||||
bool checkIsConnected() override { return bleServer && bleServer->getConnectedCount() > 0; }
|
||||
|
||||
private:
|
||||
void runOnceHandleToPhoneQueue()
|
||||
{
|
||||
if (!onReadCallbackIsWaitingForData) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0};
|
||||
size_t numBytes = getFromRadio(fromRadioBytes);
|
||||
|
||||
if (numBytes > 0 && toPhoneQueueSize < kBluetoothToPhoneQueueSize) {
|
||||
std::lock_guard<std::mutex> guard(toPhoneMutex);
|
||||
const size_t storeAtIndex = toPhoneQueueSize.load();
|
||||
memcpy(toPhoneQueue[storeAtIndex].data(), fromRadioBytes, numBytes);
|
||||
toPhoneQueueByteSizes[storeAtIndex] = numBytes;
|
||||
toPhoneQueueSize++;
|
||||
}
|
||||
|
||||
onReadCallbackIsWaitingForData = false;
|
||||
}
|
||||
|
||||
void runOnceHandleFromPhoneQueue()
|
||||
{
|
||||
if (fromPhoneQueueSize == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
BLEValue val;
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(fromPhoneMutex);
|
||||
val = fromPhoneQueue[0];
|
||||
for (size_t i = 1; i < fromPhoneQueueSize; ++i) {
|
||||
fromPhoneQueue[i - 1] = fromPhoneQueue[i];
|
||||
}
|
||||
fromPhoneQueueSize--;
|
||||
}
|
||||
|
||||
handleToRadio(val.getData(), val.getLength());
|
||||
}
|
||||
};
|
||||
|
||||
static HostedBluetoothPhoneAPI *bluetoothPhoneAPI = nullptr;
|
||||
uint8_t lastToRadio[MAX_TO_FROM_RADIO_SIZE] = {0};
|
||||
|
||||
class HostedBluetoothToRadioCallback : public BLECharacteristicCallbacks
|
||||
{
|
||||
public:
|
||||
void onWrite(BLECharacteristic *pCharacteristic) override
|
||||
{
|
||||
if (!bluetoothPhoneAPI) {
|
||||
return;
|
||||
}
|
||||
|
||||
BLEValue val;
|
||||
val.setValue(pCharacteristic->getData(), pCharacteristic->getLength());
|
||||
|
||||
if (memcmp(lastToRadio, val.getData(), val.getLength()) == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (bluetoothPhoneAPI->fromPhoneQueueSize >= kBluetoothFromPhoneQueueSize) {
|
||||
LOG_WARN("Hosted BLE onWrite drop: queue full (%u bytes)", val.getLength());
|
||||
return;
|
||||
}
|
||||
|
||||
memcpy(lastToRadio, val.getData(), val.getLength());
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(bluetoothPhoneAPI->fromPhoneMutex);
|
||||
bluetoothPhoneAPI->fromPhoneQueue.at(bluetoothPhoneAPI->fromPhoneQueueSize) = val;
|
||||
bluetoothPhoneAPI->fromPhoneQueueSize++;
|
||||
}
|
||||
|
||||
bluetoothPhoneAPI->setIntervalFromNow(0);
|
||||
concurrency::mainDelay.interrupt();
|
||||
}
|
||||
};
|
||||
|
||||
class HostedBluetoothFromRadioCallback : public BLECharacteristicCallbacks
|
||||
{
|
||||
public:
|
||||
void onRead(BLECharacteristic *pCharacteristic) override
|
||||
{
|
||||
if (!bluetoothPhoneAPI) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (bluetoothPhoneAPI->toPhoneQueueSize == 0) {
|
||||
bluetoothPhoneAPI->onReadCallbackIsWaitingForData = true;
|
||||
bluetoothPhoneAPI->setIntervalFromNow(0);
|
||||
concurrency::mainDelay.interrupt();
|
||||
|
||||
int tries = 0;
|
||||
while (bluetoothPhoneAPI->onReadCallbackIsWaitingForData && tries < 4000) {
|
||||
delay(tries < 20 ? 1 : 5);
|
||||
tries++;
|
||||
if (tries == 4000) {
|
||||
LOG_WARN("BLE onRead: timeout waiting for data after %d tries, giving up and returning 0-size response",
|
||||
tries);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0};
|
||||
size_t numBytes = 0;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(bluetoothPhoneAPI->toPhoneMutex);
|
||||
size_t pending = bluetoothPhoneAPI->toPhoneQueueSize.load();
|
||||
if (pending > 0) {
|
||||
numBytes = bluetoothPhoneAPI->toPhoneQueueByteSizes[0];
|
||||
memcpy(fromRadioBytes, bluetoothPhoneAPI->toPhoneQueue[0].data(), numBytes);
|
||||
|
||||
for (size_t i = 1; i < pending; ++i) {
|
||||
memcpy(bluetoothPhoneAPI->toPhoneQueue[i - 1].data(), bluetoothPhoneAPI->toPhoneQueue[i].data(),
|
||||
bluetoothPhoneAPI->toPhoneQueueByteSizes[i]);
|
||||
bluetoothPhoneAPI->toPhoneQueueByteSizes[i - 1] = bluetoothPhoneAPI->toPhoneQueueByteSizes[i];
|
||||
}
|
||||
|
||||
bluetoothPhoneAPI->toPhoneQueueSize--;
|
||||
}
|
||||
}
|
||||
|
||||
pCharacteristic->setValue(fromRadioBytes, numBytes);
|
||||
}
|
||||
};
|
||||
|
||||
class HostedBluetoothServerCallback : public BLEServerCallbacks
|
||||
{
|
||||
public:
|
||||
explicit HostedBluetoothServerCallback(HostedBluetooth *owner) : owner(owner) {}
|
||||
|
||||
private:
|
||||
HostedBluetooth *owner;
|
||||
|
||||
void onConnect(BLEServer *pServer, struct ble_gap_conn_desc *desc) override
|
||||
{
|
||||
BLEAddress peerAddr(desc->peer_id_addr);
|
||||
LOG_INFO("Hosted BLE incoming connection %s", peerAddr.toString().c_str());
|
||||
owner->setConnected(true);
|
||||
hostedConnHandle = desc->conn_handle;
|
||||
|
||||
const int dataLenResult = ble_gap_set_data_len(desc->conn_handle, kPreferredBleTxOctets, kPreferredBleTxTimeUs);
|
||||
if (dataLenResult != 0) {
|
||||
LOG_WARN("Hosted BLE failed to raise data length rc=%d", dataLenResult);
|
||||
}
|
||||
|
||||
pServer->updateConnParams(desc->conn_handle, 6, 12, 0, 200);
|
||||
}
|
||||
|
||||
void onDisconnect(BLEServer *pServer, struct ble_gap_conn_desc *desc) override
|
||||
{
|
||||
(void)pServer;
|
||||
(void)desc;
|
||||
LOG_INFO("Hosted BLE disconnected");
|
||||
owner->setConnected(false);
|
||||
meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED);
|
||||
bluetoothStatus->updateStatus(&newStatus);
|
||||
clearPairingDisplay();
|
||||
hostedConnHandle = BLE_HS_CONN_HANDLE_NONE;
|
||||
memset(lastToRadio, 0, sizeof(lastToRadio));
|
||||
|
||||
if (bluetoothPhoneAPI) {
|
||||
bluetoothPhoneAPI->close();
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(bluetoothPhoneAPI->fromPhoneMutex);
|
||||
bluetoothPhoneAPI->fromPhoneQueueSize = 0;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(bluetoothPhoneAPI->toPhoneMutex);
|
||||
bluetoothPhoneAPI->toPhoneQueueSize = 0;
|
||||
}
|
||||
bluetoothPhoneAPI->onReadCallbackIsWaitingForData = false;
|
||||
}
|
||||
|
||||
owner->startAdvertising();
|
||||
}
|
||||
};
|
||||
|
||||
class HostedBluetoothSecurityCallback : public BLESecurityCallbacks
|
||||
{
|
||||
public:
|
||||
void onPassKeyNotify(uint32_t passkey) override
|
||||
{
|
||||
LOG_INFO("*** Enter passkey %06u on the peer side ***", passkey);
|
||||
powerFSM.trigger(EVENT_BLUETOOTH_PAIR);
|
||||
|
||||
meshtastic::BluetoothStatus newStatus(std::to_string(passkey));
|
||||
bluetoothStatus->updateStatus(&newStatus);
|
||||
|
||||
#if HAS_SCREEN
|
||||
if (screen) {
|
||||
screen->startAlert([passkey](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void {
|
||||
char btPIN[16] = "888888";
|
||||
snprintf(btPIN, sizeof(btPIN), "%06u", passkey);
|
||||
int x_offset = display->width() / 2;
|
||||
int y_offset = display->height() <= 80 ? 0 : 12;
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->setFont(FONT_MEDIUM);
|
||||
display->drawString(x_offset + x, y_offset + y, "Bluetooth");
|
||||
#if !defined(M5STACK_UNITC6L)
|
||||
display->setFont(FONT_SMALL);
|
||||
y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5;
|
||||
display->drawString(x_offset + x, y_offset + y, "Enter this code");
|
||||
#endif
|
||||
display->setFont(FONT_LARGE);
|
||||
char pin[8];
|
||||
snprintf(pin, sizeof(pin), "%.3s %.3s", btPIN, btPIN + 3);
|
||||
y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5;
|
||||
display->drawString(x_offset + x, y_offset + y, pin);
|
||||
|
||||
display->setFont(FONT_SMALL);
|
||||
char deviceName[64];
|
||||
snprintf(deviceName, sizeof(deviceName), "Name: %s", getDeviceName());
|
||||
y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5;
|
||||
display->drawString(x_offset + x, y_offset + y, deviceName);
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
passkeyShowing = true;
|
||||
}
|
||||
|
||||
void onAuthenticationComplete(ble_gap_conn_desc *desc) override
|
||||
{
|
||||
(void)desc;
|
||||
LOG_INFO("Hosted BLE authentication complete");
|
||||
|
||||
meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED);
|
||||
bluetoothStatus->updateStatus(&newStatus);
|
||||
clearPairingDisplay();
|
||||
}
|
||||
};
|
||||
|
||||
HostedBluetoothToRadioCallback *toRadioCallbacks = nullptr;
|
||||
HostedBluetoothFromRadioCallback *fromRadioCallbacks = nullptr;
|
||||
HostedBluetoothSecurityCallback *securityCallbacks = nullptr;
|
||||
|
||||
gpio_num_t getSlaveResetGpio()
|
||||
{
|
||||
#if defined(CONFIG_ESP_HOSTED_GPIO_SLAVE_RESET_SLAVE)
|
||||
return static_cast<gpio_num_t>(CONFIG_ESP_HOSTED_GPIO_SLAVE_RESET_SLAVE);
|
||||
#elif defined(CONFIG_ESP_HOSTED_SDIO_GPIO_RESET_SLAVE)
|
||||
return static_cast<gpio_num_t>(CONFIG_ESP_HOSTED_SDIO_GPIO_RESET_SLAVE);
|
||||
#else
|
||||
return GPIO_NUM_NC;
|
||||
#endif
|
||||
}
|
||||
|
||||
void setSlaveResetLine(bool assertReset)
|
||||
{
|
||||
const gpio_num_t gpioNum = getSlaveResetGpio();
|
||||
if (gpioNum == GPIO_NUM_NC || gpioNum < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (gpio_reset_pin(gpioNum) != ESP_OK) {
|
||||
return;
|
||||
}
|
||||
if (gpio_set_direction(gpioNum, GPIO_MODE_OUTPUT) != ESP_OK) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int activeLevel =
|
||||
#if defined(CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_HIGH)
|
||||
1;
|
||||
#else
|
||||
0;
|
||||
#endif
|
||||
const int inactiveLevel = activeLevel ? 0 : 1;
|
||||
|
||||
gpio_set_level(gpioNum, assertReset ? activeLevel : inactiveLevel);
|
||||
LOG_DEBUG("[HostedBluetooth] setSlaveResetLine: GPIO[%d] -> %d (assertReset=%d, activeLevel=%d)", gpioNum,
|
||||
assertReset ? activeLevel : inactiveLevel, assertReset, activeLevel);
|
||||
}
|
||||
|
||||
void hostedEventHandler(void *arg, esp_event_base_t eventBase, int32_t eventId, void *eventData)
|
||||
{
|
||||
(void)eventData;
|
||||
|
||||
auto *self = static_cast<HostedBluetooth *>(arg);
|
||||
if (!self || eventBase != ESP_HOSTED_EVENT) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (eventId) {
|
||||
case ESP_HOSTED_EVENT_TRANSPORT_UP:
|
||||
LOG_INFO("ESP-Hosted transport is up");
|
||||
self->setConnected(true);
|
||||
break;
|
||||
case ESP_HOSTED_EVENT_TRANSPORT_DOWN:
|
||||
LOG_WARN("ESP-Hosted transport is down");
|
||||
[[fallthrough]];
|
||||
case ESP_HOSTED_EVENT_TRANSPORT_FAILURE:
|
||||
if (eventId == ESP_HOSTED_EVENT_TRANSPORT_FAILURE) {
|
||||
LOG_ERROR("ESP-Hosted transport failure");
|
||||
}
|
||||
self->setConnected(false);
|
||||
break;
|
||||
case ESP_HOSTED_EVENT_CP_INIT:
|
||||
case ESP_HOSTED_EVENT_CP_HEARTBEAT:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
HostedBluetooth::HostedBluetooth() {}
|
||||
|
||||
HostedBluetooth::~HostedBluetooth()
|
||||
{
|
||||
deinit();
|
||||
}
|
||||
|
||||
bool HostedBluetooth::registerCallbacks()
|
||||
{
|
||||
if (callbacksRegistered) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Defensive cleanup in case setup() is called again after an incomplete/early previous init.
|
||||
// esp_event_handler_unregister can safely fail if the handler wasn't present.
|
||||
esp_event_handler_unregister(ESP_HOSTED_EVENT, ESP_EVENT_ANY_ID, hostedEventHandler);
|
||||
|
||||
esp_err_t err = esp_event_handler_register(ESP_HOSTED_EVENT, ESP_EVENT_ANY_ID, hostedEventHandler, this);
|
||||
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
|
||||
LOG_ERROR("Failed to register hosted event handler: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
if (err == ESP_ERR_INVALID_STATE) {
|
||||
LOG_WARN("Hosted event handler already registered, continuing");
|
||||
}
|
||||
|
||||
callbacksRegistered = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void HostedBluetooth::unregisterCallbacks()
|
||||
{
|
||||
if (!callbacksRegistered) {
|
||||
return;
|
||||
}
|
||||
|
||||
esp_event_handler_unregister(ESP_HOSTED_EVENT, ESP_EVENT_ANY_ID, hostedEventHandler);
|
||||
|
||||
callbacksRegistered = false;
|
||||
}
|
||||
|
||||
void HostedBluetooth::setup()
|
||||
{
|
||||
if (active) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure co-processor is released from reset before bringing transport back up.
|
||||
setSlaveResetLine(false);
|
||||
LOG_DEBUG("[HostedBluetooth] setup(): Released co-processor from reset");
|
||||
|
||||
if (!registerCallbacks()) {
|
||||
return;
|
||||
}
|
||||
|
||||
active = setupGatt();
|
||||
firstRssiLogged.store(false);
|
||||
if (active) {
|
||||
LOG_INFO("ESP-Hosted Bluetooth ready");
|
||||
} else {
|
||||
LOG_ERROR("Hosted BLE setup failed in hosted mode");
|
||||
deinit();
|
||||
}
|
||||
}
|
||||
|
||||
void HostedBluetooth::shutdown()
|
||||
{
|
||||
deinit();
|
||||
}
|
||||
|
||||
void HostedBluetooth::deinit()
|
||||
{
|
||||
if (!active && !callbacksRegistered) {
|
||||
return;
|
||||
}
|
||||
|
||||
shutdownGatt();
|
||||
if (BLEDevice::getInitialized()) {
|
||||
BLEDevice::deinit(false);
|
||||
} else {
|
||||
hostedDeinitBLE();
|
||||
}
|
||||
|
||||
// Hold co-processor in reset when hosted transport is disabled to reduce idle draw.
|
||||
setSlaveResetLine(true);
|
||||
|
||||
unregisterCallbacks();
|
||||
|
||||
connected.store(false);
|
||||
active = false;
|
||||
rssi.store(0);
|
||||
firstRssiLogged.store(false);
|
||||
}
|
||||
|
||||
void HostedBluetooth::clearBonds()
|
||||
{
|
||||
ble_store_util_delete_all(BLE_STORE_OBJ_TYPE_PEER_SEC, nullptr);
|
||||
ble_store_util_delete_all(BLE_STORE_OBJ_TYPE_CCCD, nullptr);
|
||||
}
|
||||
|
||||
bool HostedBluetooth::isActive()
|
||||
{
|
||||
return active;
|
||||
}
|
||||
|
||||
bool HostedBluetooth::isConnected()
|
||||
{
|
||||
if (!connected.load()) {
|
||||
return false;
|
||||
}
|
||||
return bleServer && bleServer->getConnectedCount() > 0;
|
||||
}
|
||||
|
||||
int HostedBluetooth::getRssi()
|
||||
{
|
||||
if (!bleServer || !isConnected()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint16_t connHandle = hostedConnHandle.load();
|
||||
if (connHandle == BLE_HS_CONN_HANDLE_NONE) {
|
||||
const auto peers = bleServer->getPeerDevices(true);
|
||||
if (!peers.empty()) {
|
||||
connHandle = peers.begin()->first;
|
||||
hostedConnHandle = connHandle;
|
||||
}
|
||||
}
|
||||
if (connHandle == BLE_HS_CONN_HANDLE_NONE) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int8_t currentRssi = 0;
|
||||
const int rc = ble_gap_conn_rssi(connHandle, ¤tRssi);
|
||||
if (rc == 0) {
|
||||
setRssi(currentRssi);
|
||||
return currentRssi;
|
||||
}
|
||||
|
||||
return rssi.load();
|
||||
}
|
||||
|
||||
void HostedBluetooth::sendLog(const uint8_t *logMessage, size_t length)
|
||||
{
|
||||
if (!logRadioCharacteristic || !isConnected() || length > 512) {
|
||||
return;
|
||||
}
|
||||
|
||||
logRadioCharacteristic->setValue(logMessage, length);
|
||||
logRadioCharacteristic->notify();
|
||||
}
|
||||
|
||||
void HostedBluetooth::setConnected(bool value)
|
||||
{
|
||||
connected.store(value);
|
||||
}
|
||||
|
||||
void HostedBluetooth::setRssi(int value)
|
||||
{
|
||||
rssi.store(value);
|
||||
maybeLogFirstRssi(value);
|
||||
}
|
||||
|
||||
void HostedBluetooth::maybeLogFirstRssi(int value)
|
||||
{
|
||||
bool expected = false;
|
||||
if (firstRssiLogged.compare_exchange_strong(expected, true)) {
|
||||
LOG_INFO("ESP-Hosted first RSSI update: %d dBm", value);
|
||||
}
|
||||
}
|
||||
|
||||
bool HostedBluetooth::setupGatt()
|
||||
{
|
||||
memset(lastToRadio, 0, sizeof(lastToRadio));
|
||||
hostedConnHandle = BLE_HS_CONN_HANDLE_NONE;
|
||||
|
||||
if (!BLEDevice::init(getDeviceName())) {
|
||||
LOG_ERROR("Hosted BLE init failed");
|
||||
return false;
|
||||
}
|
||||
#ifdef ESP_PWR_LVL_P9
|
||||
BLEDevice::setPower(ESP_PWR_LVL_P9);
|
||||
#endif
|
||||
|
||||
BLESecurity *security = new BLESecurity();
|
||||
security->setInitEncryptionKey(ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK);
|
||||
security->setRespEncryptionKey(ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK);
|
||||
if (config.bluetooth.mode != meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) {
|
||||
security->setCapability(ESP_IO_CAP_OUT);
|
||||
if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN) {
|
||||
LOG_INFO("Hosted BLE using random passkey");
|
||||
security->setPassKey(false);
|
||||
} else {
|
||||
LOG_INFO("Hosted BLE using fixed passkey");
|
||||
security->setPassKey(true, config.bluetooth.fixed_pin);
|
||||
}
|
||||
// Enable authorization requirements:
|
||||
// - bonding: true (for persistent storage of the keys)
|
||||
// - MITM: true (enables Man-In-The-Middle protection for password prompts)
|
||||
// - secure connection: true (enables secure connection for encryption)
|
||||
security->setAuthenticationMode(true, true, true);
|
||||
} else {
|
||||
security->setCapability(ESP_IO_CAP_NONE);
|
||||
security->setAuthenticationMode(true, false, false);
|
||||
}
|
||||
|
||||
if (!securityCallbacks) {
|
||||
securityCallbacks = new HostedBluetoothSecurityCallback();
|
||||
}
|
||||
BLEDevice::setSecurityCallbacks(securityCallbacks);
|
||||
|
||||
const int mtuResult = BLEDevice::setMTU(kPreferredBleMtu);
|
||||
if (mtuResult == 0) {
|
||||
LOG_INFO("Hosted BLE MTU request set to %u", kPreferredBleMtu);
|
||||
} else {
|
||||
LOG_WARN("Hosted BLE unable to request MTU %u, rc=%d", kPreferredBleMtu, mtuResult);
|
||||
}
|
||||
|
||||
bleServer = BLEDevice::createServer();
|
||||
if (!bleServer) {
|
||||
LOG_ERROR("Hosted BLE createServer failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
const int nameRc = ble_svc_gap_device_name_set(BLEDevice::getDeviceName().c_str());
|
||||
if (nameRc != 0) {
|
||||
LOG_WARN("Hosted BLE device_name_set rc=%d %s", nameRc, BLEUtils::returnCodeToString(nameRc));
|
||||
}
|
||||
|
||||
bleServer->setCallbacks(new HostedBluetoothServerCallback(this));
|
||||
|
||||
BLEService *meshService = bleServer->createService(MESH_SERVICE_UUID);
|
||||
if (!meshService) {
|
||||
LOG_ERROR("Hosted BLE mesh service creation failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
BLECharacteristic *toRadioCharacteristic = nullptr;
|
||||
BLECharacteristic *fromRadioCharacteristic = nullptr;
|
||||
if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) {
|
||||
toRadioCharacteristic = meshService->createCharacteristic(TORADIO_UUID, BLECharacteristic::PROPERTY_WRITE);
|
||||
fromRadioCharacteristic = meshService->createCharacteristic(FROMRADIO_UUID, BLECharacteristic::PROPERTY_READ);
|
||||
fromNumCharacteristic = meshService->createCharacteristic(FROMNUM_UUID, BLECharacteristic::PROPERTY_NOTIFY |
|
||||
BLECharacteristic::PROPERTY_READ);
|
||||
logRadioCharacteristic = meshService->createCharacteristic(LOGRADIO_UUID, BLECharacteristic::PROPERTY_NOTIFY |
|
||||
BLECharacteristic::PROPERTY_READ);
|
||||
} else {
|
||||
toRadioCharacteristic = meshService->createCharacteristic(TORADIO_UUID, BLECharacteristic::PROPERTY_WRITE |
|
||||
BLECharacteristic::PROPERTY_WRITE_AUTHEN |
|
||||
BLECharacteristic::PROPERTY_WRITE_ENC);
|
||||
fromRadioCharacteristic = meshService->createCharacteristic(FROMRADIO_UUID, BLECharacteristic::PROPERTY_READ |
|
||||
BLECharacteristic::PROPERTY_READ_AUTHEN |
|
||||
BLECharacteristic::PROPERTY_READ_ENC);
|
||||
fromNumCharacteristic = meshService->createCharacteristic(
|
||||
FROMNUM_UUID, BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_READ |
|
||||
BLECharacteristic::PROPERTY_READ_AUTHEN | BLECharacteristic::PROPERTY_READ_ENC);
|
||||
logRadioCharacteristic = meshService->createCharacteristic(
|
||||
LOGRADIO_UUID, BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_READ |
|
||||
BLECharacteristic::PROPERTY_READ_AUTHEN | BLECharacteristic::PROPERTY_READ_ENC);
|
||||
}
|
||||
|
||||
if (!toRadioCharacteristic || !fromRadioCharacteristic || !fromNumCharacteristic || !logRadioCharacteristic) {
|
||||
LOG_ERROR("Hosted BLE characteristic creation failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!bluetoothPhoneAPI) {
|
||||
bluetoothPhoneAPI = new HostedBluetoothPhoneAPI();
|
||||
}
|
||||
|
||||
if (!toRadioCallbacks) {
|
||||
toRadioCallbacks = new HostedBluetoothToRadioCallback();
|
||||
}
|
||||
if (!fromRadioCallbacks) {
|
||||
fromRadioCallbacks = new HostedBluetoothFromRadioCallback();
|
||||
}
|
||||
|
||||
toRadioCharacteristic->setCallbacks(toRadioCallbacks);
|
||||
fromRadioCharacteristic->setCallbacks(fromRadioCallbacks);
|
||||
meshService->start();
|
||||
|
||||
startAdvertising();
|
||||
return true;
|
||||
}
|
||||
|
||||
void HostedBluetooth::shutdownGatt()
|
||||
{
|
||||
if (bluetoothPhoneAPI) {
|
||||
bluetoothPhoneAPI->close();
|
||||
delete bluetoothPhoneAPI;
|
||||
bluetoothPhoneAPI = nullptr;
|
||||
}
|
||||
|
||||
delete toRadioCallbacks;
|
||||
toRadioCallbacks = nullptr;
|
||||
|
||||
delete fromRadioCallbacks;
|
||||
fromRadioCallbacks = nullptr;
|
||||
|
||||
delete securityCallbacks;
|
||||
securityCallbacks = nullptr;
|
||||
|
||||
if (bleServer) {
|
||||
BLEAdvertising *advertising = BLEDevice::getAdvertising();
|
||||
if (advertising) {
|
||||
advertising->stop();
|
||||
}
|
||||
}
|
||||
|
||||
fromNumCharacteristic = nullptr;
|
||||
logRadioCharacteristic = nullptr;
|
||||
bleServer = nullptr;
|
||||
hostedConnHandle = BLE_HS_CONN_HANDLE_NONE;
|
||||
}
|
||||
|
||||
void HostedBluetooth::startAdvertising()
|
||||
{
|
||||
BLEAdvertising *advertising = BLEDevice::getAdvertising();
|
||||
if (!advertising) {
|
||||
LOG_ERROR("Hosted BLE getAdvertising failed");
|
||||
return;
|
||||
}
|
||||
|
||||
advertising->stop();
|
||||
advertising->reset();
|
||||
advertising->addServiceUUID(MESH_SERVICE_UUID);
|
||||
|
||||
BLEAdvertisementData scan;
|
||||
scan.setName(getDeviceName());
|
||||
advertising->setScanResponseData(scan);
|
||||
advertising->setMinPreferred(0x06);
|
||||
advertising->setMaxPreferred(0x12);
|
||||
|
||||
if (!advertising->start(0)) {
|
||||
LOG_ERROR("Hosted BLE failed to start advertising");
|
||||
} else {
|
||||
LOG_INFO("Hosted BLE advertising started");
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include "BluetoothCommon.h"
|
||||
#include <atomic>
|
||||
|
||||
/**
|
||||
* Placeholder backend for ESP32-P4 + ESP-Hosted BLE transport.
|
||||
*
|
||||
* This intentionally mirrors the NimbleBluetooth surface used by the firmware,
|
||||
* so the caller side can stay stable while we implement esp-hosted internals.
|
||||
*/
|
||||
class HostedBluetooth : public BluetoothApi
|
||||
{
|
||||
public:
|
||||
HostedBluetooth();
|
||||
~HostedBluetooth() override;
|
||||
|
||||
void setup() override;
|
||||
void shutdown() override;
|
||||
void deinit() override;
|
||||
void clearBonds() override;
|
||||
bool isActive() override;
|
||||
bool isConnected() override;
|
||||
int getRssi() override;
|
||||
void sendLog(const uint8_t *logMessage, size_t length) override;
|
||||
void startAdvertising();
|
||||
|
||||
void setConnected(bool value);
|
||||
void setRssi(int value);
|
||||
|
||||
private:
|
||||
bool setupGatt();
|
||||
void shutdownGatt();
|
||||
void maybeLogFirstRssi(int value);
|
||||
bool registerCallbacks();
|
||||
void unregisterCallbacks();
|
||||
|
||||
bool active = false;
|
||||
std::atomic<bool> connected{false};
|
||||
std::atomic<int> rssi{0};
|
||||
std::atomic<bool> firstRssiLogged{false};
|
||||
bool callbacksRegistered = false;
|
||||
};
|
||||
@@ -206,6 +206,9 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||
{"KZ_863", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KZ_863},
|
||||
{"NP_865", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_NP_865},
|
||||
{"BR_902", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_BR_902},
|
||||
{"ITU1_2M (144-146)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M},
|
||||
{"ITU2_2M (144-148)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU2_2M},
|
||||
{"ITU3_2M (144-148)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU3_2M},
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -6,15 +6,12 @@
|
||||
FastEPD buffer format: 1bpp, horizontal bytes, MSB = leftmost pixel, 1 = white
|
||||
|
||||
Both formats share the same pixel layout and polarity (1 = white, 0 = black).
|
||||
The InkHUD safe-area buffer (944×523) is copied into the centre of the physical
|
||||
The InkHUD safe-area buffer (928×508) is copied into the centre of the physical
|
||||
960×540 FastEPD buffer so content clears the panel's inactive edge border.
|
||||
See ED047TC1.h for the H_OFFSET_BYTES / V_OFFSET_TOP / V_OFFSET_BOTTOM constants.
|
||||
|
||||
*/
|
||||
|
||||
// Ruler diagnostic — uncomment to draw calibration lines at each physical edge.
|
||||
// #define EINK_EDGE_LINES
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
#ifdef T5_S3_EPAPER_PRO
|
||||
|
||||
@@ -212,35 +209,6 @@ void ED047TC1::update(uint8_t *imageData, UpdateTypes type)
|
||||
memcpy(dstRow, srcRow, srcRowBytes);
|
||||
}
|
||||
|
||||
#ifdef EINK_EDGE_LINES
|
||||
// Draw a 1px black box at the exact boundary of the safe area within the
|
||||
// physical buffer. If the margins are correct, all 4 lines should be
|
||||
// fully visible and right at the edge of the usable display area.
|
||||
|
||||
auto setPixelBlack = [&](uint32_t col, uint32_t row) { cur[row * dstRowBytes + col / 8] &= ~(0x80 >> (col % 8)); };
|
||||
|
||||
const uint32_t safeX = H_OFFSET_BYTES * 8;
|
||||
const uint32_t safeY = V_OFFSET_TOP;
|
||||
const uint32_t safeW = DISPLAY_WIDTH;
|
||||
const uint32_t safeH = DISPLAY_HEIGHT;
|
||||
|
||||
// Top edge: horizontal line at safeY
|
||||
for (uint32_t col = safeX; col < safeX + safeW; col++)
|
||||
setPixelBlack(col, safeY);
|
||||
|
||||
// Bottom edge: horizontal line at safeY + safeH - 1
|
||||
for (uint32_t col = safeX; col < safeX + safeW; col++)
|
||||
setPixelBlack(col, safeY + safeH - 1);
|
||||
|
||||
// Left edge: vertical line at safeX
|
||||
for (uint32_t row = safeY; row < safeY + safeH; row++)
|
||||
setPixelBlack(safeX, row);
|
||||
|
||||
// Right edge: vertical line at safeX + safeW - 1
|
||||
for (uint32_t row = safeY; row < safeY + safeH; row++)
|
||||
setPixelBlack(safeX + safeW - 1, row);
|
||||
#endif
|
||||
|
||||
if (type == FULL) {
|
||||
epaper->fullUpdate(CLEAR_SLOW, false);
|
||||
epaper->backupPlane(); // Sync pPrevious so next partialUpdate has a correct baseline
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
V_OFFSET_TOP and V_OFFSET_BOTTOM (vertical, pixel rows) to position it.
|
||||
|
||||
Changing these constants shifts content inward from each physical edge:
|
||||
H_OFFSET_BYTES = 1 → 8px left margin, 8px right margin (960 – 8 – 8 = 944)
|
||||
V_OFFSET_TOP = 9 → 9px top margin (asymmetric: top ≠ bottom)
|
||||
V_OFFSET_BOTTOM = 8 → 8px bottom margin (540 – 9 – 8 = 523)
|
||||
H_OFFSET_BYTES = 2 → 16px left margin, 16px right margin (960 – 16 – 16 = 928)
|
||||
V_OFFSET_TOP = 16 → 16px top margin
|
||||
V_OFFSET_BOTTOM = 16 → 16px bottom margin (540 – 16 – 16 = 508)
|
||||
|
||||
*/
|
||||
|
||||
@@ -61,13 +61,13 @@ class ED047TC1 : public EInk
|
||||
//
|
||||
// Calibrated by flashing a 1px border box and adjusting until all 4 sides are visible.
|
||||
|
||||
static constexpr uint16_t DISPLAY_WIDTH = 944; // 960 − H_OFFSET_BYTES×8 − right_margin (8+8 = 16px)
|
||||
static constexpr uint16_t DISPLAY_HEIGHT = 523; // 540 − V_OFFSET_TOP − V_OFFSET_BOTTOM (9+8 = 17px)
|
||||
static constexpr uint16_t DISPLAY_WIDTH = 928; // 960 − H_OFFSET_BYTES×8 − right_margin (16+16 = 32px)
|
||||
static constexpr uint16_t DISPLAY_HEIGHT = 508; // 540 − V_OFFSET_TOP − V_OFFSET_BOTTOM (16+16 = 32px)
|
||||
|
||||
static constexpr uint8_t H_OFFSET_BYTES = 1; // visual TOP : 8px physical left margin
|
||||
// visual BOTTOM: 960−8−944=8px physical right margin
|
||||
static constexpr uint8_t V_OFFSET_TOP = 9; // visual RIGHT : CONFIRMED OK
|
||||
static constexpr uint8_t V_OFFSET_BOTTOM = 8; // visual LEFT : 8px physical bottom margin
|
||||
static constexpr uint8_t H_OFFSET_BYTES = 2; // visual TOP : 16px physical left margin
|
||||
// visual BOTTOM: 960−16−928=16px physical right margin
|
||||
static constexpr uint8_t V_OFFSET_TOP = 16; // visual RIGHT : 16px physical top margin
|
||||
static constexpr uint8_t V_OFFSET_BOTTOM = 16; // visual LEFT : 16px physical bottom margin
|
||||
|
||||
static constexpr UpdateTypes supported = static_cast<UpdateTypes>(FULL | FAST);
|
||||
|
||||
|
||||
@@ -66,6 +66,9 @@ enum MenuAction {
|
||||
SET_REGION_BR_902,
|
||||
SET_REGION_EU_866,
|
||||
SET_REGION_NARROW_868,
|
||||
SET_REGION_ITU1_2M,
|
||||
SET_REGION_ITU2_2M,
|
||||
SET_REGION_ITU3_2M,
|
||||
// Device Roles
|
||||
SET_ROLE_CLIENT,
|
||||
SET_ROLE_CLIENT_MUTE,
|
||||
@@ -84,6 +87,8 @@ enum MenuAction {
|
||||
SET_PRESET_LITE_FAST,
|
||||
SET_PRESET_NARROW_SLOW,
|
||||
SET_PRESET_NARROW_FAST,
|
||||
SET_PRESET_TINY_SLOW,
|
||||
SET_PRESET_TINY_FAST,
|
||||
SET_PRESET_FROM_REGION, // Dynamic: preset chosen from region-available list
|
||||
// Timezones
|
||||
SET_TZ_US_HAWAII,
|
||||
|
||||
@@ -784,6 +784,18 @@ void InkHUD::MenuApplet::execute(MenuItem item)
|
||||
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_N_868);
|
||||
break;
|
||||
|
||||
case SET_REGION_ITU1_2M:
|
||||
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M);
|
||||
break;
|
||||
|
||||
case SET_REGION_ITU2_2M:
|
||||
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_ITU2_2M);
|
||||
break;
|
||||
|
||||
case SET_REGION_ITU3_2M:
|
||||
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_ITU3_2M);
|
||||
break;
|
||||
|
||||
// Roles
|
||||
case SET_ROLE_CLIENT:
|
||||
applyDeviceRole(meshtastic_Config_DeviceConfig_Role_CLIENT);
|
||||
@@ -834,6 +846,22 @@ void InkHUD::MenuApplet::execute(MenuItem item)
|
||||
applyLoRaPreset(PRESET(SHORT_TURBO));
|
||||
break;
|
||||
|
||||
case SET_PRESET_NARROW_SLOW:
|
||||
applyLoRaPreset(PRESET(NARROW_SLOW));
|
||||
break;
|
||||
|
||||
case SET_PRESET_NARROW_FAST:
|
||||
applyLoRaPreset(PRESET(NARROW_FAST));
|
||||
break;
|
||||
|
||||
case SET_PRESET_TINY_SLOW:
|
||||
applyLoRaPreset(PRESET(TINY_SLOW));
|
||||
break;
|
||||
|
||||
case SET_PRESET_TINY_FAST:
|
||||
applyLoRaPreset(PRESET(TINY_FAST));
|
||||
break;
|
||||
|
||||
case SET_PRESET_FROM_REGION: {
|
||||
// cursor - 1 because index 0 is "Back"
|
||||
const uint8_t index = cursor - 1;
|
||||
@@ -1469,6 +1497,9 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
items.push_back(MenuItem("KZ 863", MenuAction::SET_REGION_KZ_863, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("NP 865", MenuAction::SET_REGION_NP_865, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("BR 902", MenuAction::SET_REGION_BR_902, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("ITU1_2M (144-146)", MenuAction::SET_REGION_ITU1_2M, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("ITU2_2M (144-148)", MenuAction::SET_REGION_ITU2_2M, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("ITU3_2M (144-148)", MenuAction::SET_REGION_ITU3_2M, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||
break;
|
||||
|
||||
|
||||
+1
-2
@@ -52,8 +52,7 @@
|
||||
#include "mesh/http/WebServer.h"
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_BLUETOOTH
|
||||
#include "nimble/NimbleBluetooth.h"
|
||||
NimbleBluetooth *nimbleBluetooth = nullptr;
|
||||
BluetoothApi *bluetoothApi = nullptr;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
|
||||
+2
-2
@@ -12,8 +12,8 @@
|
||||
#include <SPI.h>
|
||||
#include <map>
|
||||
#if defined(ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2)
|
||||
#include "nimble/NimbleBluetooth.h"
|
||||
extern NimbleBluetooth *nimbleBluetooth;
|
||||
#include "BluetoothCommon.h"
|
||||
extern BluetoothApi *bluetoothApi;
|
||||
#endif
|
||||
#ifdef ARCH_NRF52
|
||||
#include "NRF52Bluetooth.h"
|
||||
|
||||
@@ -37,6 +37,12 @@ enum class TrafficType { POSITION, TELEMETRY };
|
||||
#define default_traffic_mgmt_position_precision_bits 24 // ~10m grid cells
|
||||
#define default_traffic_mgmt_position_min_interval_secs (ONE_DAY / 2) // 12 hours between identical positions
|
||||
|
||||
// Hop scaling defaults
|
||||
#define default_hop_scaling_min_target_nodes 40 // walk threshold: first hop reaching this cumulative count
|
||||
#define default_hop_scaling_max_target_nodes 80 // generous extension ceiling (2 × min)
|
||||
#define default_hop_scaling_min_target_nodes_floor 5 // minimum allowed min_target_nodes
|
||||
#define default_hop_scaling_max_target_nodes_ceiling 512 // maximum allowed max_target_nodes
|
||||
|
||||
#ifdef USERPREFS_RINGTONE_NAG_SECS
|
||||
#define default_ringtone_nag_secs USERPREFS_RINGTONE_NAG_SECS
|
||||
#else
|
||||
|
||||
+32
-1
@@ -44,7 +44,7 @@ extern const RegionProfile PROFILE_EU868;
|
||||
extern const RegionProfile PROFILE_UNDEF;
|
||||
extern const RegionProfile PROFILE_LITE;
|
||||
extern const RegionProfile PROFILE_NARROW;
|
||||
// extern const RegionProfile PROFILE_HAM;
|
||||
extern const RegionProfile PROFILE_HAM_20KHZ;
|
||||
|
||||
// Map from old region names to new region enums
|
||||
struct RegionInfo {
|
||||
@@ -77,6 +77,7 @@ extern const RegionInfo regions[];
|
||||
extern const RegionInfo *myRegion;
|
||||
|
||||
extern void initRegion();
|
||||
extern const RegionInfo *getRegion(meshtastic_Config_LoRaConfig_RegionCode code);
|
||||
|
||||
// Valid LoRa spread factor range and defaults
|
||||
constexpr uint8_t LORA_SF_MIN = 5;
|
||||
@@ -123,8 +124,18 @@ static inline float clampBandwidthKHz(float bwKHz)
|
||||
|
||||
static inline float bwCodeToKHz(uint16_t bwCode)
|
||||
{
|
||||
if (bwCode == 8)
|
||||
return 7.8f;
|
||||
if (bwCode == 10)
|
||||
return 10.4f;
|
||||
if (bwCode == 16)
|
||||
return 15.6f;
|
||||
if (bwCode == 21)
|
||||
return 20.8f;
|
||||
if (bwCode == 31)
|
||||
return 31.25f;
|
||||
if (bwCode == 42)
|
||||
return 41.7f;
|
||||
if (bwCode == 62)
|
||||
return 62.5f;
|
||||
if (bwCode == 200)
|
||||
@@ -140,8 +151,18 @@ static inline float bwCodeToKHz(uint16_t bwCode)
|
||||
|
||||
static inline uint16_t bwKHzToCode(float bwKHz)
|
||||
{
|
||||
if (bwKHz > 7.7f && bwKHz < 7.9f)
|
||||
return 8;
|
||||
if (bwKHz > 10.3f && bwKHz < 10.5f)
|
||||
return 10;
|
||||
if (bwKHz > 15.5f && bwKHz < 15.7f)
|
||||
return 16;
|
||||
if (bwKHz > 20.7f && bwKHz < 20.9f)
|
||||
return 21;
|
||||
if (bwKHz > 31.24f && bwKHz < 31.26f)
|
||||
return 31;
|
||||
if (bwKHz > 41.6f && bwKHz < 41.8f)
|
||||
return 42;
|
||||
if (bwKHz > 62.49f && bwKHz < 62.51f)
|
||||
return 62;
|
||||
if (bwKHz > 203.12f && bwKHz < 203.13f)
|
||||
@@ -219,6 +240,16 @@ static inline void modemPresetToParams(meshtastic_Config_LoRaConfig_ModemPreset
|
||||
cr = 6;
|
||||
sf = 8;
|
||||
break;
|
||||
case PRESET(TINY_FAST):
|
||||
bwKHz = 15.6f;
|
||||
cr = 5;
|
||||
sf = 7;
|
||||
break;
|
||||
case PRESET(TINY_SLOW):
|
||||
bwKHz = 15.6f;
|
||||
cr = 6;
|
||||
sf = 8;
|
||||
break;
|
||||
default: // LONG_FAST (or illegal)
|
||||
bwKHz = wideLora ? 812.5f : 250.0f;
|
||||
cr = 5;
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
#include "mesh/generated/meshtastic/deviceonly_legacy.pb.h"
|
||||
#include "meshUtils.h"
|
||||
#include "modules/NeighborInfoModule.h"
|
||||
#if HAS_VARIABLE_HOPS
|
||||
#include "modules/HopScalingModule.h"
|
||||
#endif
|
||||
#include "xmodem.h"
|
||||
#include <ErriezCRC32.h>
|
||||
#include <algorithm>
|
||||
@@ -2538,6 +2541,14 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp)
|
||||
nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_VIA_MQTT_MASK,
|
||||
mp.via_mqtt); // Store if we received this packet via MQTT
|
||||
|
||||
#if HAS_VARIABLE_HOPS
|
||||
// Only sample packets that arrived over LoRa.
|
||||
if (mp.transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA && hopScalingModule) {
|
||||
uint8_t hopCount = std::max(int8_t(0), getHopsAway(mp));
|
||||
hopScalingModule->samplePacketForHistogram(mp.from, hopCount);
|
||||
}
|
||||
#endif
|
||||
|
||||
// If hopStart was set and there wasn't someone messing with the limit in the middle, add hopsAway
|
||||
const int8_t hopsAway = getHopsAway(mp);
|
||||
if (hopsAway >= 0) {
|
||||
|
||||
@@ -49,6 +49,8 @@ static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_LITE[] = {PRESET(L
|
||||
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_NARROW[] = {PRESET(NARROW_FAST), PRESET(NARROW_SLOW),
|
||||
MODEM_PRESET_END};
|
||||
|
||||
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_TINY[] = {PRESET(TINY_FAST), PRESET(TINY_SLOW), MODEM_PRESET_END};
|
||||
|
||||
// Region profiles: bundle preset list + regulatory parameters shared across regions
|
||||
// presets, spacing, padding, audio, licensed, text throttle, position throttle, telemetry throttle
|
||||
const RegionProfile PROFILE_STD = {PRESETS_STD, 0, 0, true, false, 0, 1, 1};
|
||||
@@ -56,6 +58,8 @@ const RegionProfile PROFILE_EU868 = {PRESETS_EU_868, 0, 0, false, false, 0, 1, 1
|
||||
const RegionProfile PROFILE_UNDEF = {PRESETS_UNDEF, 0, 0, true, false, 0, 1, 1};
|
||||
const RegionProfile PROFILE_LITE = {PRESETS_LITE, 0.4, 0.0375f, false, false, 0, 10, 10};
|
||||
const RegionProfile PROFILE_NARROW = {PRESETS_NARROW, 0, 0.0104f, true, false, 0, 1, 1};
|
||||
// Ham '20kHz' profile. 15.6kHz bandwidth coerced to 20kHz via padding.
|
||||
const RegionProfile PROFILE_HAM_20KHZ = {PRESETS_TINY, 0, 0.0022f, false, true, 0, 2, 2};
|
||||
|
||||
#define RDEF(name, freq_start, freq_end, duty_cycle, power_limit, frequency_switching, wide_lora, profile_ptr, default_preset, \
|
||||
override_slot) \
|
||||
@@ -226,6 +230,35 @@ const RegionInfo regions[] = {
|
||||
*/
|
||||
RDEF(BR_902, 902.0f, 907.5f, 100, 30, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
|
||||
/*
|
||||
ITU Region 1 (Europe, Africa, Middle East, former USSR) amateur 2m allocation: 144.000 - 146.000 MHz.
|
||||
Power limit is the regulatory ceiling (1 W / 30 dBm) — individual hardware will cap below this
|
||||
via its own PA curve; the field here is just the legal upper bound.
|
||||
|
||||
Default slot: 26 (144.510 MHz)
|
||||
https://www.iaru-r1.org/wp-content/uploads/2020/12/VHF-Bandplan.pdf
|
||||
*/
|
||||
RDEF(ITU1_2M, 144.0f, 146.0f, 100, 30, false, false, PROFILE_HAM_20KHZ, PRESET(TINY_FAST), 26),
|
||||
|
||||
/*
|
||||
ITU Region 2 (Americas) amateur 2m allocation: 144.000 - 148.000 MHz.
|
||||
Typical admin rules (e.g. US FCC Part 97) allow well above 30 dBm for licensed operators.
|
||||
|
||||
Default slot: 51 (145.010 MHz)
|
||||
https://www.arrl.org/band-plan
|
||||
*/
|
||||
RDEF(ITU2_2M, 144.0f, 148.0f, 100, 30, false, false, PROFILE_HAM_20KHZ, PRESET(TINY_FAST), 51),
|
||||
|
||||
/*
|
||||
ITU Region 3 (Asia/Pacific) amateur 2m allocation: 144.000 - 148.000 MHz.
|
||||
Typical admin rules allow well above 30 dBm for licensed operators.
|
||||
|
||||
Default slot: 33 (144.650 MHz)
|
||||
https://www.iaru.org/wp-content/uploads/2020/01/R3-004-IARU-Region-3-Bandplan-rev.2.pdf
|
||||
https://www.wia.org.au/members/bandplans/data/documents/WIA%20Australian%20Band%20Plan%202026.pdf
|
||||
*/
|
||||
RDEF(ITU3_2M, 144.0f, 148.0f, 100, 30, false, false, PROFILE_HAM_20KHZ, PRESET(TINY_FAST), 33),
|
||||
|
||||
/*
|
||||
2.4 GHZ WLAN Band equivalent. Only for SX128x chips.
|
||||
*/
|
||||
@@ -834,6 +867,16 @@ bool RadioInterface::validateConfigRegion(const meshtastic_Config_LoRaConfig &lo
|
||||
{
|
||||
const RegionInfo *newRegion = getRegion(loraConfig.region);
|
||||
|
||||
// Reject unrecognized region codes (getRegion returns UNSET sentinel for unknown codes)
|
||||
if (newRegion->code != loraConfig.region) {
|
||||
char err_string[160];
|
||||
snprintf(err_string, sizeof(err_string), "Region code %d is not recognized", loraConfig.region);
|
||||
LOG_ERROR("%s", err_string);
|
||||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
||||
sendErrorNotification(err_string);
|
||||
return false;
|
||||
}
|
||||
|
||||
// If you are not licensed, you can't use ham regions.
|
||||
if (newRegion->profile->licensedOnly && !devicestate.owner.is_licensed) {
|
||||
char err_string[160];
|
||||
|
||||
+29
-10
@@ -15,6 +15,9 @@
|
||||
#if HAS_TRAFFIC_MANAGEMENT
|
||||
#include "modules/TrafficManagementModule.h"
|
||||
#endif
|
||||
#if HAS_VARIABLE_HOPS
|
||||
#include "modules/HopScalingModule.h"
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_MQTT
|
||||
#include "mqtt/MQTT.h"
|
||||
#endif
|
||||
@@ -22,8 +25,6 @@
|
||||
#if ARCH_PORTDUINO
|
||||
#include "Throttle.h"
|
||||
#include "platform/portduino/PortduinoGlue.h"
|
||||
#endif
|
||||
#if ENABLE_JSON_LOGGING || ARCH_PORTDUINO
|
||||
#include "serialization/MeshPacketSerializer.h"
|
||||
#endif
|
||||
|
||||
@@ -357,6 +358,30 @@ ErrorCode Router::send(meshtastic_MeshPacket *p)
|
||||
p->from = getFrom(p);
|
||||
|
||||
p->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum()); // set the relayer to us
|
||||
|
||||
#if HAS_VARIABLE_HOPS
|
||||
// Apply HopScaling hop recommendation to routine outgoing broadcasts
|
||||
if (isFromUs(p) && isBroadcast(p->to) && hopScalingModule && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
|
||||
switch (p->decoded.portnum) {
|
||||
case meshtastic_PortNum_POSITION_APP:
|
||||
case meshtastic_PortNum_TELEMETRY_APP:
|
||||
case meshtastic_PortNum_NODEINFO_APP:
|
||||
case meshtastic_PortNum_NEIGHBORINFO_APP: {
|
||||
uint8_t variableHopLimit = hopScalingModule->getLastRequiredHop();
|
||||
|
||||
// Never exceed user-configured hop_limit
|
||||
if (variableHopLimit < p->hop_limit) {
|
||||
LOG_DEBUG("[HOPSCALE] hop_limit %u -> %u for portnum %u", p->hop_limit, variableHopLimit, p->decoded.portnum);
|
||||
p->hop_limit = variableHopLimit;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// If we are the original transmitter, set the hop limit with which we start
|
||||
if (isFromUs(p))
|
||||
p->hop_start = p->hop_limit;
|
||||
@@ -555,9 +580,7 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
} */
|
||||
|
||||
printPacket("decoded message", p);
|
||||
#if ENABLE_JSON_LOGGING
|
||||
LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str());
|
||||
#elif ARCH_PORTDUINO
|
||||
#if ARCH_PORTDUINO
|
||||
if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) {
|
||||
LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str());
|
||||
} else if (portduino_config.JSONFilename != "") {
|
||||
@@ -852,11 +875,7 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
|
||||
|
||||
void Router::perhapsHandleReceived(meshtastic_MeshPacket *p)
|
||||
{
|
||||
#if ENABLE_JSON_LOGGING
|
||||
// Even ignored packets get logged in the trace
|
||||
p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone
|
||||
LOG_TRACE("%s", MeshPacketSerializer::JsonSerializeEncrypted(p).c_str());
|
||||
#elif ARCH_PORTDUINO
|
||||
#if ARCH_PORTDUINO
|
||||
// Even ignored packets get logged in the trace
|
||||
if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) {
|
||||
p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone
|
||||
|
||||
@@ -198,6 +198,28 @@ typedef struct _meshtastic_LockdownAuth {
|
||||
way to reset the session clock is a reboot, which costs a boot
|
||||
from the on-flash, HMAC-bound counter. */
|
||||
uint32_t max_session_seconds;
|
||||
/* Disable lockdown mode. Requires a valid passphrase in the same
|
||||
message (the device must prove the operator owns it before
|
||||
reverting at-rest encryption). On success the firmware decrypts
|
||||
every stored config / channel / nodedb file back to plaintext,
|
||||
removes the wrapped DEK, unlock token, monotonic-counter, and
|
||||
backoff files, and reboots out of lockdown.
|
||||
|
||||
This is the inverse of the provision/unlock path: it is how the
|
||||
client app's "lockdown mode" toggle returns a device to normal
|
||||
operation.
|
||||
|
||||
NOT reversed by this operation: APPROTECT. Once the debug port
|
||||
lockout has been burned (on silicon where it is effective) it is
|
||||
permanent — disabling lockdown decrypts your data and removes the
|
||||
access gates, but the SWD/JTAG port stays locked for the life of
|
||||
the device (recoverable only via a full chip erase over a debug
|
||||
probe, which destroys all data). Clients should make this
|
||||
irreversibility clear at the moment lockdown is first enabled.
|
||||
|
||||
When true the passphrase field is still required; boots_remaining,
|
||||
valid_until_epoch, max_session_seconds, and lock_now are ignored. */
|
||||
bool disable;
|
||||
} meshtastic_LockdownAuth;
|
||||
|
||||
/* Parameters for setting up Meshtastic for ameteur radio usage */
|
||||
@@ -521,7 +543,7 @@ extern "C" {
|
||||
#define meshtastic_AdminMessage_init_default {0, {0}, {0, {0}}}
|
||||
#define meshtastic_AdminMessage_InputEvent_init_default {0, 0, 0, 0}
|
||||
#define meshtastic_AdminMessage_OTAEvent_init_default {_meshtastic_OTAMode_MIN, {0, {0}}}
|
||||
#define meshtastic_LockdownAuth_init_default {{0, {0}}, 0, 0, 0, 0}
|
||||
#define meshtastic_LockdownAuth_init_default {{0, {0}}, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_HamParameters_init_default {"", 0, 0, ""}
|
||||
#define meshtastic_NodeRemoteHardwarePinsResponse_init_default {0, {meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default}}
|
||||
#define meshtastic_SharedContact_init_default {0, false, meshtastic_User_init_default, 0, 0}
|
||||
@@ -534,7 +556,7 @@ extern "C" {
|
||||
#define meshtastic_AdminMessage_init_zero {0, {0}, {0, {0}}}
|
||||
#define meshtastic_AdminMessage_InputEvent_init_zero {0, 0, 0, 0}
|
||||
#define meshtastic_AdminMessage_OTAEvent_init_zero {_meshtastic_OTAMode_MIN, {0, {0}}}
|
||||
#define meshtastic_LockdownAuth_init_zero {{0, {0}}, 0, 0, 0, 0}
|
||||
#define meshtastic_LockdownAuth_init_zero {{0, {0}}, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_HamParameters_init_zero {"", 0, 0, ""}
|
||||
#define meshtastic_NodeRemoteHardwarePinsResponse_init_zero {0, {meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero}}
|
||||
#define meshtastic_SharedContact_init_zero {0, false, meshtastic_User_init_zero, 0, 0}
|
||||
@@ -557,6 +579,7 @@ extern "C" {
|
||||
#define meshtastic_LockdownAuth_valid_until_epoch_tag 3
|
||||
#define meshtastic_LockdownAuth_lock_now_tag 4
|
||||
#define meshtastic_LockdownAuth_max_session_seconds_tag 5
|
||||
#define meshtastic_LockdownAuth_disable_tag 6
|
||||
#define meshtastic_HamParameters_call_sign_tag 1
|
||||
#define meshtastic_HamParameters_tx_power_tag 2
|
||||
#define meshtastic_HamParameters_frequency_tag 3
|
||||
@@ -754,7 +777,8 @@ X(a, STATIC, SINGULAR, BYTES, passphrase, 1) \
|
||||
X(a, STATIC, SINGULAR, UINT32, boots_remaining, 2) \
|
||||
X(a, STATIC, SINGULAR, UINT32, valid_until_epoch, 3) \
|
||||
X(a, STATIC, SINGULAR, BOOL, lock_now, 4) \
|
||||
X(a, STATIC, SINGULAR, UINT32, max_session_seconds, 5)
|
||||
X(a, STATIC, SINGULAR, UINT32, max_session_seconds, 5) \
|
||||
X(a, STATIC, SINGULAR, BOOL, disable, 6)
|
||||
#define meshtastic_LockdownAuth_CALLBACK NULL
|
||||
#define meshtastic_LockdownAuth_DEFAULT NULL
|
||||
|
||||
@@ -869,7 +893,7 @@ extern const pb_msgdesc_t meshtastic_SHTXX_config_msg;
|
||||
#define meshtastic_AdminMessage_size 511
|
||||
#define meshtastic_HamParameters_size 31
|
||||
#define meshtastic_KeyVerificationAdmin_size 25
|
||||
#define meshtastic_LockdownAuth_size 54
|
||||
#define meshtastic_LockdownAuth_size 56
|
||||
#define meshtastic_NodeRemoteHardwarePinsResponse_size 496
|
||||
#define meshtastic_SCD30_config_size 27
|
||||
#define meshtastic_SCD4X_config_size 29
|
||||
|
||||
@@ -356,7 +356,21 @@ typedef enum _meshtastic_Config_LoRaConfig_ModemPreset {
|
||||
/* Narrow Slow
|
||||
Moderate range preset optimized for EU 868MHz band with 62.5kHz bandwidth.
|
||||
Comparable link budget and data rate to LONG_FAST. */
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_NARROW_SLOW = 13
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_NARROW_SLOW = 13,
|
||||
/* Tiny Fast
|
||||
Preset optimized for compliance with Amateur Radio restrictions with 20kHz bandwidth.
|
||||
Many regions limit data transmission bandwidth in lower amateur bands (2 Meter).
|
||||
Note: TCXO with tight tolerances (±5 ppm or better) is *absolutely required* at these narrow bandwidths.
|
||||
Only compatible with SX127x and SX126x chipsets.
|
||||
Comparable link budget and data rate to LONG_FAST. */
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_TINY_FAST = 14,
|
||||
/* Tiny Slow
|
||||
Preset optimized for compliance with Amateur Radio restrictions with 20kHz bandwidth.
|
||||
Many regions limit data transmission bandwidth in lower amateur bands (2 Meter).
|
||||
Note: TCXO with tight tolerances (±5 ppm or better) is *absolutely required* at these narrow bandwidths.
|
||||
Only compatible with SX127x and SX126x chipsets.
|
||||
Comparable link budget and data rate to LONG_MODERATE. */
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_TINY_SLOW = 15
|
||||
} meshtastic_Config_LoRaConfig_ModemPreset;
|
||||
|
||||
typedef enum _meshtastic_Config_LoRaConfig_FEM_LNA_Mode {
|
||||
@@ -749,8 +763,8 @@ extern "C" {
|
||||
#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_ITU3_70CM+1))
|
||||
|
||||
#define _meshtastic_Config_LoRaConfig_ModemPreset_MIN meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST
|
||||
#define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_NARROW_SLOW
|
||||
#define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_NARROW_SLOW+1))
|
||||
#define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_TINY_SLOW
|
||||
#define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_TINY_SLOW+1))
|
||||
|
||||
#define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED
|
||||
#define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MAX meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT
|
||||
|
||||
@@ -658,7 +658,14 @@ typedef enum _meshtastic_LockdownStatus_State {
|
||||
token's TTL. */
|
||||
meshtastic_LockdownStatus_State_UNLOCKED = 3,
|
||||
/* Passphrase rejected. backoff_seconds is non-zero when rate-limited. */
|
||||
meshtastic_LockdownStatus_State_UNLOCK_FAILED = 4
|
||||
meshtastic_LockdownStatus_State_UNLOCK_FAILED = 4,
|
||||
/* Lockdown is supported by this firmware but not currently active
|
||||
(no passphrase has been provisioned, or it was disabled via
|
||||
AdminMessage.lockdown_auth.disable). The device is operating in
|
||||
normal, non-encrypted mode. Clients render the lockdown-mode
|
||||
toggle as OFF on receiving this. Distinct from NEEDS_PROVISION,
|
||||
which is only used during an in-progress enable flow. */
|
||||
meshtastic_LockdownStatus_State_DISABLED = 5
|
||||
} meshtastic_LockdownStatus_State;
|
||||
|
||||
/* Struct definitions */
|
||||
@@ -1533,8 +1540,8 @@ extern "C" {
|
||||
#define _meshtastic_LogRecord_Level_ARRAYSIZE ((meshtastic_LogRecord_Level)(meshtastic_LogRecord_Level_CRITICAL+1))
|
||||
|
||||
#define _meshtastic_LockdownStatus_State_MIN meshtastic_LockdownStatus_State_STATE_UNSPECIFIED
|
||||
#define _meshtastic_LockdownStatus_State_MAX meshtastic_LockdownStatus_State_UNLOCK_FAILED
|
||||
#define _meshtastic_LockdownStatus_State_ARRAYSIZE ((meshtastic_LockdownStatus_State)(meshtastic_LockdownStatus_State_UNLOCK_FAILED+1))
|
||||
#define _meshtastic_LockdownStatus_State_MAX meshtastic_LockdownStatus_State_DISABLED
|
||||
#define _meshtastic_LockdownStatus_State_ARRAYSIZE ((meshtastic_LockdownStatus_State)(meshtastic_LockdownStatus_State_DISABLED+1))
|
||||
|
||||
#define meshtastic_Position_location_source_ENUMTYPE meshtastic_Position_LocSource
|
||||
#define meshtastic_Position_altitude_source_ENUMTYPE meshtastic_Position_AltSource
|
||||
|
||||
+253
-174
@@ -11,11 +11,12 @@
|
||||
#endif
|
||||
#include "SPILock.h"
|
||||
#include "power.h"
|
||||
#include "serialization/JSON.h"
|
||||
#include <FSCommon.h>
|
||||
#include <HTTPBodyParser.hpp>
|
||||
#include <HTTPMultipartBodyParser.hpp>
|
||||
#include <HTTPURLEncodedBodyParser.hpp>
|
||||
#include <cmath>
|
||||
#include <sstream>
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
#include "esp_task_wdt.h"
|
||||
@@ -259,40 +260,95 @@ void htmlDeleteDir(const char *dirname)
|
||||
root.close();
|
||||
}
|
||||
|
||||
JSONArray htmlListDir(const char *dirname, uint8_t levels)
|
||||
// Escape a string into a JSON double-quoted literal. Matches the previous
|
||||
// SimpleJSON StringifyString behavior (0x00-0x1F and 0x7F -> \u00xx lowercase,
|
||||
// escapes " \ / \b \f \n \r \t, UTF-8 passes through unchanged).
|
||||
static std::string jsonEscape(const std::string &str)
|
||||
{
|
||||
std::string out = "\"";
|
||||
for (size_t i = 0; i < str.size(); ++i) {
|
||||
char chr = str[i];
|
||||
if (chr == '"' || chr == '\\' || chr == '/') {
|
||||
out += '\\';
|
||||
out += chr;
|
||||
} else if (chr == '\b') {
|
||||
out += "\\b";
|
||||
} else if (chr == '\f') {
|
||||
out += "\\f";
|
||||
} else if (chr == '\n') {
|
||||
out += "\\n";
|
||||
} else if (chr == '\r') {
|
||||
out += "\\r";
|
||||
} else if (chr == '\t') {
|
||||
out += "\\t";
|
||||
} else if ((unsigned char)chr < 0x20 || chr == 0x7F) {
|
||||
char buf[8];
|
||||
snprintf(buf, sizeof(buf), "\\u%04x", (unsigned char)chr);
|
||||
out += buf;
|
||||
} else {
|
||||
out += chr;
|
||||
}
|
||||
}
|
||||
out += "\"";
|
||||
return out;
|
||||
}
|
||||
|
||||
// Format a numeric value the way the previous SimpleJSON serializer did
|
||||
// (std::stringstream with precision 15, NaN/Inf -> "null").
|
||||
static std::string jsonNum(double v)
|
||||
{
|
||||
if (std::isinf(v) || std::isnan(v))
|
||||
return "null";
|
||||
std::ostringstream ss;
|
||||
ss.precision(15);
|
||||
ss << v;
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// Build a serialized JSON array string listing files in `dirname`.
|
||||
// Subdirectories recurse as nested arrays (up to `levels` deep).
|
||||
std::string htmlListDir(const char *dirname, uint8_t levels)
|
||||
{
|
||||
File root = FSCom.open(dirname, FILE_O_READ);
|
||||
JSONArray fileList;
|
||||
std::string out = "[";
|
||||
bool first = true;
|
||||
if (!root) {
|
||||
return fileList;
|
||||
out += "]";
|
||||
return out;
|
||||
}
|
||||
if (!root.isDirectory()) {
|
||||
return fileList;
|
||||
out += "]";
|
||||
return out;
|
||||
}
|
||||
|
||||
// iterate over the file list
|
||||
File file = root.openNextFile();
|
||||
while (file) {
|
||||
std::string element;
|
||||
bool haveElement = false;
|
||||
if (file.isDirectory() && !String(file.name()).endsWith(".")) {
|
||||
if (levels) {
|
||||
#ifdef ARCH_ESP32
|
||||
fileList.push_back(new JSONValue(htmlListDir(file.path(), levels - 1)));
|
||||
element = htmlListDir(file.path(), levels - 1);
|
||||
#else
|
||||
fileList.push_back(new JSONValue(htmlListDir(file.name(), levels - 1)));
|
||||
element = htmlListDir(file.name(), levels - 1);
|
||||
#endif
|
||||
haveElement = true;
|
||||
file.close();
|
||||
}
|
||||
} else {
|
||||
JSONObject thisFileMap;
|
||||
thisFileMap["size"] = new JSONValue((int)file.size());
|
||||
#ifdef ARCH_ESP32
|
||||
String fileName = String(file.path()).substring(1);
|
||||
thisFileMap["name"] = new JSONValue(fileName.c_str());
|
||||
#else
|
||||
String fileName = String(file.name()).substring(1);
|
||||
thisFileMap["name"] = new JSONValue(fileName.c_str());
|
||||
#endif
|
||||
String tempName = String(file.name()).substring(1);
|
||||
// Keys in the previous std::map<string,...> were emitted in
|
||||
// alphabetical order: name, nameModified, size.
|
||||
element = "{";
|
||||
element += jsonEscape("name");
|
||||
element += ":";
|
||||
element += jsonEscape(fileName.c_str());
|
||||
if (tempName.endsWith(".gz")) {
|
||||
#ifdef ARCH_ESP32
|
||||
String modifiedFile = String(file.path()).substring(1);
|
||||
@@ -300,15 +356,30 @@ JSONArray htmlListDir(const char *dirname, uint8_t levels)
|
||||
String modifiedFile = String(file.name()).substring(1);
|
||||
#endif
|
||||
modifiedFile.remove((modifiedFile.length() - 3), 3);
|
||||
thisFileMap["nameModified"] = new JSONValue(modifiedFile.c_str());
|
||||
element += ",";
|
||||
element += jsonEscape("nameModified");
|
||||
element += ":";
|
||||
element += jsonEscape(modifiedFile.c_str());
|
||||
}
|
||||
fileList.push_back(new JSONValue(thisFileMap));
|
||||
element += ",";
|
||||
element += jsonEscape("size");
|
||||
element += ":";
|
||||
element += jsonNum((int)file.size());
|
||||
element += "}";
|
||||
haveElement = true;
|
||||
}
|
||||
if (haveElement) {
|
||||
if (!first)
|
||||
out += ",";
|
||||
out += element;
|
||||
first = false;
|
||||
}
|
||||
file.close();
|
||||
file = root.openNextFile();
|
||||
}
|
||||
root.close();
|
||||
return fileList;
|
||||
out += "]";
|
||||
return out;
|
||||
}
|
||||
|
||||
void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res)
|
||||
@@ -318,28 +389,25 @@ void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res)
|
||||
res->setHeader("Access-Control-Allow-Methods", "GET");
|
||||
|
||||
concurrency::LockGuard g(spiLock);
|
||||
auto fileList = htmlListDir("/static", 10);
|
||||
std::string fileList = htmlListDir("/static", 10);
|
||||
|
||||
// create json output structure
|
||||
JSONObject filesystemObj;
|
||||
filesystemObj["total"] = new JSONValue((int)FSCom.totalBytes());
|
||||
filesystemObj["used"] = new JSONValue((int)FSCom.usedBytes());
|
||||
filesystemObj["free"] = new JSONValue(int(FSCom.totalBytes() - FSCom.usedBytes()));
|
||||
uint64_t total = FSCom.totalBytes();
|
||||
uint64_t used = FSCom.usedBytes();
|
||||
|
||||
JSONObject jsonObjInner;
|
||||
jsonObjInner["files"] = new JSONValue(fileList);
|
||||
jsonObjInner["filesystem"] = new JSONValue(filesystemObj);
|
||||
// Key order matches the previous std::map-based emission (alphabetical).
|
||||
std::string out;
|
||||
out.reserve(fileList.size() + 128);
|
||||
out += "{\"data\":{\"files\":";
|
||||
out += fileList;
|
||||
out += ",\"filesystem\":{\"free\":";
|
||||
out += jsonNum((int)(total - used));
|
||||
out += ",\"total\":";
|
||||
out += jsonNum((int)total);
|
||||
out += ",\"used\":";
|
||||
out += jsonNum((int)used);
|
||||
out += "}},\"status\":\"ok\"}";
|
||||
|
||||
JSONObject jsonObjOuter;
|
||||
jsonObjOuter["data"] = new JSONValue(jsonObjInner);
|
||||
jsonObjOuter["status"] = new JSONValue("ok");
|
||||
|
||||
JSONValue *value = new JSONValue(jsonObjOuter);
|
||||
|
||||
std::string jsonString = value->Stringify();
|
||||
res->print(jsonString.c_str());
|
||||
|
||||
delete value;
|
||||
res->print(out.c_str());
|
||||
}
|
||||
|
||||
void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res)
|
||||
@@ -354,27 +422,13 @@ void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res)
|
||||
if (params->getQueryParameter("delete", paramValDelete)) {
|
||||
std::string pathDelete = "/" + paramValDelete;
|
||||
concurrency::LockGuard g(spiLock);
|
||||
if (FSCom.remove(pathDelete.c_str())) {
|
||||
|
||||
LOG_INFO("%s", pathDelete.c_str());
|
||||
JSONObject jsonObjOuter;
|
||||
jsonObjOuter["status"] = new JSONValue("ok");
|
||||
JSONValue *value = new JSONValue(jsonObjOuter);
|
||||
std::string jsonString = value->Stringify();
|
||||
res->print(jsonString.c_str());
|
||||
delete value;
|
||||
return;
|
||||
} else {
|
||||
|
||||
LOG_INFO("%s", pathDelete.c_str());
|
||||
JSONObject jsonObjOuter;
|
||||
jsonObjOuter["status"] = new JSONValue("Error");
|
||||
JSONValue *value = new JSONValue(jsonObjOuter);
|
||||
std::string jsonString = value->Stringify();
|
||||
res->print(jsonString.c_str());
|
||||
delete value;
|
||||
return;
|
||||
}
|
||||
const char *status = FSCom.remove(pathDelete.c_str()) ? "ok" : "Error";
|
||||
LOG_INFO("%s", pathDelete.c_str());
|
||||
std::string out = "{\"status\":";
|
||||
out += jsonEscape(status);
|
||||
out += "}";
|
||||
res->print(out.c_str());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -615,95 +669,113 @@ void handleReport(HTTPRequest *req, HTTPResponse *res)
|
||||
res->println("<pre>");
|
||||
}
|
||||
|
||||
// Helper lambda to create JSON array and clean up memory properly
|
||||
auto createJSONArrayFromLog = [](const uint32_t *logArray, int count) -> JSONValue * {
|
||||
JSONArray tempArray;
|
||||
auto arrayFromLog = [](const uint32_t *logArray, int count) -> std::string {
|
||||
std::string s = "[";
|
||||
for (int i = 0; i < count; i++) {
|
||||
tempArray.push_back(new JSONValue((int)logArray[i]));
|
||||
if (i)
|
||||
s += ",";
|
||||
s += jsonNum((int)logArray[i]);
|
||||
}
|
||||
JSONValue *result = new JSONValue(tempArray);
|
||||
// Note: Don't delete tempArray elements here - JSONValue now owns them
|
||||
return result;
|
||||
s += "]";
|
||||
return s;
|
||||
};
|
||||
|
||||
// data->airtime->tx_log
|
||||
uint32_t *logArray;
|
||||
logArray = airTime->airtimeReport(TX_LOG);
|
||||
JSONValue *txLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
|
||||
|
||||
// data->airtime->rx_log
|
||||
std::string txLog = arrayFromLog(logArray, airTime->getPeriodsToLog());
|
||||
logArray = airTime->airtimeReport(RX_LOG);
|
||||
JSONValue *rxLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
|
||||
|
||||
// data->airtime->rx_all_log
|
||||
std::string rxLog = arrayFromLog(logArray, airTime->getPeriodsToLog());
|
||||
logArray = airTime->airtimeReport(RX_ALL_LOG);
|
||||
JSONValue *rxAllLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
|
||||
std::string rxAllLog = arrayFromLog(logArray, airTime->getPeriodsToLog());
|
||||
|
||||
// data->airtime
|
||||
JSONObject jsonObjAirtime;
|
||||
jsonObjAirtime["tx_log"] = txLogJsonValue;
|
||||
jsonObjAirtime["rx_log"] = rxLogJsonValue;
|
||||
jsonObjAirtime["rx_all_log"] = rxAllLogJsonValue;
|
||||
jsonObjAirtime["channel_utilization"] = new JSONValue(airTime->channelUtilizationPercent());
|
||||
jsonObjAirtime["utilization_tx"] = new JSONValue(airTime->utilizationTXPercent());
|
||||
jsonObjAirtime["seconds_since_boot"] = new JSONValue(int(airTime->getSecondsSinceBoot()));
|
||||
jsonObjAirtime["seconds_per_period"] = new JSONValue(int(airTime->getSecondsPerPeriod()));
|
||||
jsonObjAirtime["periods_to_log"] = new JSONValue(airTime->getPeriodsToLog());
|
||||
|
||||
// data->wifi
|
||||
JSONObject jsonObjWifi;
|
||||
jsonObjWifi["rssi"] = new JSONValue(WiFi.RSSI());
|
||||
String wifiIPString = WiFi.localIP().toString();
|
||||
std::string wifiIP = wifiIPString.c_str();
|
||||
jsonObjWifi["ip"] = new JSONValue(wifiIP.c_str());
|
||||
|
||||
// data->memory
|
||||
JSONObject jsonObjMemory;
|
||||
jsonObjMemory["heap_total"] = new JSONValue((int)memGet.getHeapSize());
|
||||
jsonObjMemory["heap_free"] = new JSONValue((int)memGet.getFreeHeap());
|
||||
jsonObjMemory["psram_total"] = new JSONValue((int)memGet.getPsramSize());
|
||||
jsonObjMemory["psram_free"] = new JSONValue((int)memGet.getFreePsram());
|
||||
spiLock->lock();
|
||||
jsonObjMemory["fs_total"] = new JSONValue((int)FSCom.totalBytes());
|
||||
jsonObjMemory["fs_used"] = new JSONValue((int)FSCom.usedBytes());
|
||||
jsonObjMemory["fs_free"] = new JSONValue(int(FSCom.totalBytes() - FSCom.usedBytes()));
|
||||
uint64_t fsTotal = FSCom.totalBytes();
|
||||
uint64_t fsUsed = FSCom.usedBytes();
|
||||
spiLock->unlock();
|
||||
|
||||
// data->power
|
||||
JSONObject jsonObjPower;
|
||||
jsonObjPower["battery_percent"] = new JSONValue(powerStatus->getBatteryChargePercent());
|
||||
jsonObjPower["battery_voltage_mv"] = new JSONValue(powerStatus->getBatteryVoltageMv());
|
||||
jsonObjPower["has_battery"] = new JSONValue(BoolToString(powerStatus->getHasBattery()));
|
||||
jsonObjPower["has_usb"] = new JSONValue(BoolToString(powerStatus->getHasUSB()));
|
||||
jsonObjPower["is_charging"] = new JSONValue(BoolToString(powerStatus->getIsCharging()));
|
||||
// Emit keys in the same alphabetical order as the previous
|
||||
// std::map-based JSON output to keep responses byte-compatible.
|
||||
std::string out;
|
||||
out.reserve(1024);
|
||||
out += "{\"data\":{";
|
||||
|
||||
// data->device
|
||||
JSONObject jsonObjDevice;
|
||||
jsonObjDevice["reboot_counter"] = new JSONValue((int)myNodeInfo.reboot_count);
|
||||
// airtime
|
||||
out += "\"airtime\":{";
|
||||
out += "\"channel_utilization\":";
|
||||
out += jsonNum(airTime->channelUtilizationPercent());
|
||||
out += ",\"periods_to_log\":";
|
||||
out += jsonNum(airTime->getPeriodsToLog());
|
||||
out += ",\"rx_all_log\":";
|
||||
out += rxAllLog;
|
||||
out += ",\"rx_log\":";
|
||||
out += rxLog;
|
||||
out += ",\"seconds_per_period\":";
|
||||
out += jsonNum((int)airTime->getSecondsPerPeriod());
|
||||
out += ",\"seconds_since_boot\":";
|
||||
out += jsonNum((int)airTime->getSecondsSinceBoot());
|
||||
out += ",\"tx_log\":";
|
||||
out += txLog;
|
||||
out += ",\"utilization_tx\":";
|
||||
out += jsonNum(airTime->utilizationTXPercent());
|
||||
out += "}";
|
||||
|
||||
// data->radio
|
||||
JSONObject jsonObjRadio;
|
||||
jsonObjRadio["frequency"] = new JSONValue(RadioLibInterface::instance->getFreq());
|
||||
jsonObjRadio["lora_channel"] = new JSONValue((int)RadioLibInterface::instance->getChannelNum() + 1);
|
||||
// device
|
||||
out += ",\"device\":{\"reboot_counter\":";
|
||||
out += jsonNum((int)myNodeInfo.reboot_count);
|
||||
out += "}";
|
||||
|
||||
// collect data to inner data object
|
||||
JSONObject jsonObjInner;
|
||||
jsonObjInner["airtime"] = new JSONValue(jsonObjAirtime);
|
||||
jsonObjInner["wifi"] = new JSONValue(jsonObjWifi);
|
||||
jsonObjInner["memory"] = new JSONValue(jsonObjMemory);
|
||||
jsonObjInner["power"] = new JSONValue(jsonObjPower);
|
||||
jsonObjInner["device"] = new JSONValue(jsonObjDevice);
|
||||
jsonObjInner["radio"] = new JSONValue(jsonObjRadio);
|
||||
// memory
|
||||
out += ",\"memory\":{";
|
||||
out += "\"fs_free\":";
|
||||
out += jsonNum((int)(fsTotal - fsUsed));
|
||||
out += ",\"fs_total\":";
|
||||
out += jsonNum((int)fsTotal);
|
||||
out += ",\"fs_used\":";
|
||||
out += jsonNum((int)fsUsed);
|
||||
out += ",\"heap_free\":";
|
||||
out += jsonNum((int)memGet.getFreeHeap());
|
||||
out += ",\"heap_total\":";
|
||||
out += jsonNum((int)memGet.getHeapSize());
|
||||
out += ",\"psram_free\":";
|
||||
out += jsonNum((int)memGet.getFreePsram());
|
||||
out += ",\"psram_total\":";
|
||||
out += jsonNum((int)memGet.getPsramSize());
|
||||
out += "}";
|
||||
|
||||
// create json output structure
|
||||
JSONObject jsonObjOuter;
|
||||
jsonObjOuter["data"] = new JSONValue(jsonObjInner);
|
||||
jsonObjOuter["status"] = new JSONValue("ok");
|
||||
// serialize and write it to the stream
|
||||
JSONValue *value = new JSONValue(jsonObjOuter);
|
||||
std::string jsonString = value->Stringify();
|
||||
res->print(jsonString.c_str());
|
||||
delete value;
|
||||
// power (has_* / is_charging were serialized as the strings "true"/"false")
|
||||
out += ",\"power\":{";
|
||||
out += "\"battery_percent\":";
|
||||
out += jsonNum(powerStatus->getBatteryChargePercent());
|
||||
out += ",\"battery_voltage_mv\":";
|
||||
out += jsonNum(powerStatus->getBatteryVoltageMv());
|
||||
out += ",\"has_battery\":";
|
||||
out += jsonEscape(BoolToString(powerStatus->getHasBattery()));
|
||||
out += ",\"has_usb\":";
|
||||
out += jsonEscape(BoolToString(powerStatus->getHasUSB()));
|
||||
out += ",\"is_charging\":";
|
||||
out += jsonEscape(BoolToString(powerStatus->getIsCharging()));
|
||||
out += "}";
|
||||
|
||||
// radio
|
||||
out += ",\"radio\":{\"frequency\":";
|
||||
out += jsonNum(RadioLibInterface::instance->getFreq());
|
||||
out += ",\"lora_channel\":";
|
||||
out += jsonNum((int)RadioLibInterface::instance->getChannelNum() + 1);
|
||||
out += "}";
|
||||
|
||||
// wifi
|
||||
out += ",\"wifi\":{\"ip\":";
|
||||
out += jsonEscape(wifiIP);
|
||||
out += ",\"rssi\":";
|
||||
out += jsonNum(WiFi.RSSI());
|
||||
out += "}";
|
||||
|
||||
out += "},\"status\":\"ok\"}";
|
||||
|
||||
res->print(out.c_str());
|
||||
}
|
||||
|
||||
void handleNodes(HTTPRequest *req, HTTPResponse *res)
|
||||
@@ -724,58 +796,66 @@ void handleNodes(HTTPRequest *req, HTTPResponse *res)
|
||||
res->println("<pre>");
|
||||
}
|
||||
|
||||
JSONArray nodesArray;
|
||||
std::string out;
|
||||
out.reserve(2048);
|
||||
out += "{\"data\":{\"nodes\":[";
|
||||
|
||||
bool firstNode = true;
|
||||
uint32_t readIndex = 0;
|
||||
const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
|
||||
while (tempNodeInfo != NULL) {
|
||||
if (nodeInfoLiteHasUser(tempNodeInfo)) {
|
||||
JSONObject node;
|
||||
|
||||
char id[16];
|
||||
snprintf(id, sizeof(id), "!%08x", tempNodeInfo->num);
|
||||
|
||||
node["id"] = new JSONValue(id);
|
||||
node["snr"] = new JSONValue(tempNodeInfo->snr);
|
||||
node["via_mqtt"] = new JSONValue(BoolToString(nodeInfoLiteViaMqtt(tempNodeInfo)));
|
||||
node["last_heard"] = new JSONValue((int)tempNodeInfo->last_heard);
|
||||
node["position"] = new JSONValue();
|
||||
|
||||
std::string position;
|
||||
if (nodeDB->hasValidPosition(tempNodeInfo)) {
|
||||
meshtastic_PositionLite posLite;
|
||||
if (nodeDB->copyNodePosition(tempNodeInfo->num, posLite)) {
|
||||
JSONObject position;
|
||||
position["latitude"] = new JSONValue((float)posLite.latitude_i * 1e-7);
|
||||
position["longitude"] = new JSONValue((float)posLite.longitude_i * 1e-7);
|
||||
position["altitude"] = new JSONValue((int)posLite.altitude);
|
||||
node["position"] = new JSONValue(position);
|
||||
position = "{\"altitude\":";
|
||||
position += jsonNum((int)posLite.altitude);
|
||||
position += ",\"latitude\":";
|
||||
position += jsonNum((float)posLite.latitude_i * 1e-7);
|
||||
position += ",\"longitude\":";
|
||||
position += jsonNum((float)posLite.longitude_i * 1e-7);
|
||||
position += "}";
|
||||
} else {
|
||||
position = "null";
|
||||
}
|
||||
} else {
|
||||
position = "null";
|
||||
}
|
||||
|
||||
node["long_name"] = new JSONValue(tempNodeInfo->long_name);
|
||||
node["short_name"] = new JSONValue(tempNodeInfo->short_name);
|
||||
// mac_address dropped from NodeInfoLite as part of the slim refactor; emit zeros.
|
||||
node["mac_address"] = new JSONValue("00:00:00:00:00:00");
|
||||
node["hw_model"] = new JSONValue(tempNodeInfo->hw_model);
|
||||
if (!firstNode)
|
||||
out += ",";
|
||||
firstNode = false;
|
||||
|
||||
nodesArray.push_back(new JSONValue(node));
|
||||
// Alphabetical key order matches previous std::map-based output.
|
||||
out += "{\"hw_model\":";
|
||||
out += jsonNum(tempNodeInfo->hw_model);
|
||||
out += ",\"id\":";
|
||||
out += jsonEscape(id);
|
||||
out += ",\"last_heard\":";
|
||||
out += jsonNum((int)tempNodeInfo->last_heard);
|
||||
out += ",\"long_name\":";
|
||||
out += jsonEscape(tempNodeInfo->long_name);
|
||||
out += ",\"mac_address\":";
|
||||
out += jsonEscape("00:00:00:00:00:00");
|
||||
out += ",\"position\":";
|
||||
out += position;
|
||||
out += ",\"short_name\":";
|
||||
out += jsonEscape(tempNodeInfo->short_name);
|
||||
out += ",\"snr\":";
|
||||
out += jsonNum(tempNodeInfo->snr);
|
||||
out += ",\"via_mqtt\":";
|
||||
out += jsonEscape(BoolToString(nodeInfoLiteViaMqtt(tempNodeInfo)));
|
||||
out += "}";
|
||||
}
|
||||
tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
|
||||
}
|
||||
|
||||
// collect data to inner data object
|
||||
JSONObject jsonObjInner;
|
||||
jsonObjInner["nodes"] = new JSONValue(nodesArray);
|
||||
|
||||
// create json output structure
|
||||
JSONObject jsonObjOuter;
|
||||
jsonObjOuter["data"] = new JSONValue(jsonObjInner);
|
||||
jsonObjOuter["status"] = new JSONValue("ok");
|
||||
// serialize and write it to the stream
|
||||
JSONValue *value = new JSONValue(jsonObjOuter);
|
||||
std::string jsonString = value->Stringify();
|
||||
res->print(jsonString.c_str());
|
||||
delete value;
|
||||
out += "]},\"status\":\"ok\"}";
|
||||
res->print(out.c_str());
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -897,20 +977,28 @@ void handleScanNetworks(HTTPRequest *req, HTTPResponse *res)
|
||||
|
||||
int n = WiFi.scanNetworks();
|
||||
|
||||
// build list of network objects
|
||||
JSONArray networkObjs;
|
||||
std::string out = "{\"data\":[";
|
||||
bool firstNet = true;
|
||||
if (n > 0) {
|
||||
for (int i = 0; i < n; ++i) {
|
||||
char ssidArray[50];
|
||||
// The previous implementation pre-escaped quotes before handing
|
||||
// the value to the JSON serializer; preserve that (byte-compatible
|
||||
// even if it double-encodes a quote) so existing clients are not
|
||||
// affected by this refactor.
|
||||
String ssidString = String(WiFi.SSID(i));
|
||||
ssidString.replace("\"", "\\\"");
|
||||
ssidString.toCharArray(ssidArray, 50);
|
||||
|
||||
if (WiFi.encryptionType(i) != WIFI_AUTH_OPEN) {
|
||||
JSONObject thisNetwork;
|
||||
thisNetwork["ssid"] = new JSONValue(ssidArray);
|
||||
thisNetwork["rssi"] = new JSONValue(int(WiFi.RSSI(i)));
|
||||
networkObjs.push_back(new JSONValue(thisNetwork));
|
||||
if (!firstNet)
|
||||
out += ",";
|
||||
firstNet = false;
|
||||
out += "{\"rssi\":";
|
||||
out += jsonNum((int)WiFi.RSSI(i));
|
||||
out += ",\"ssid\":";
|
||||
out += jsonEscape(ssidArray);
|
||||
out += "}";
|
||||
}
|
||||
// Yield some cpu cycles to IP stack.
|
||||
// This is important in case the list is large and it takes us time to return
|
||||
@@ -918,16 +1006,7 @@ void handleScanNetworks(HTTPRequest *req, HTTPResponse *res)
|
||||
yield();
|
||||
}
|
||||
}
|
||||
|
||||
// build output structure
|
||||
JSONObject jsonObjOuter;
|
||||
jsonObjOuter["data"] = new JSONValue(networkObjs);
|
||||
jsonObjOuter["status"] = new JSONValue("ok");
|
||||
|
||||
// serialize and write it to the stream
|
||||
JSONValue *value = new JSONValue(jsonObjOuter);
|
||||
std::string jsonString = value->Stringify();
|
||||
res->print(jsonString.c_str());
|
||||
delete value;
|
||||
out += "],\"status\":\"ok\"}";
|
||||
res->print(out.c_str());
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -109,6 +109,15 @@ static inline int get_max_num_nodes()
|
||||
#define HAS_TRAFFIC_MANAGEMENT 0
|
||||
#endif
|
||||
|
||||
// HopScalingModule - variable hop module: dynamically adjusts broadcast hop_limit based on mesh density
|
||||
// Enable per-variant by defining HAS_VARIABLE_HOPS=1 in variant.h
|
||||
#ifdef ARCH_STM32WL
|
||||
#define HAS_VARIABLE_HOPS 0
|
||||
#endif
|
||||
#ifndef HAS_VARIABLE_HOPS
|
||||
#define HAS_VARIABLE_HOPS 1
|
||||
#endif
|
||||
|
||||
// Cache size for traffic management (number of nodes to track)
|
||||
// Can be overridden per-variant based on available memory
|
||||
#ifndef TRAFFIC_MANAGEMENT_CACHE_SIZE
|
||||
|
||||
@@ -66,4 +66,10 @@ inline uint32_t pow_of_2(uint32_t n)
|
||||
return 1 << n;
|
||||
}
|
||||
|
||||
/// Returns true if n is a power of two (n >= 1).
|
||||
template <typename T> constexpr bool is_pow_of_2(T n)
|
||||
{
|
||||
return n >= T(1) && (n & (n - T(1))) == T(0);
|
||||
}
|
||||
|
||||
#define IS_ONE_OF(item, ...) isOneOf(item, sizeof((int[]){__VA_ARGS__}) / sizeof(int), __VA_ARGS__)
|
||||
|
||||
@@ -1338,9 +1338,9 @@ void AdminModule::handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &r
|
||||
conn.has_bluetooth = true;
|
||||
conn.bluetooth.pin = config.bluetooth.fixed_pin;
|
||||
#ifdef ARCH_ESP32
|
||||
if (config.bluetooth.enabled && nimbleBluetooth) {
|
||||
conn.bluetooth.is_connected = nimbleBluetooth->isConnected();
|
||||
conn.bluetooth.rssi = nimbleBluetooth->getRssi();
|
||||
if (config.bluetooth.enabled && bluetoothApi) {
|
||||
conn.bluetooth.is_connected = bluetoothApi->isConnected();
|
||||
conn.bluetooth.rssi = bluetoothApi->getRssi();
|
||||
}
|
||||
#elif defined(ARCH_NRF52)
|
||||
if (config.bluetooth.enabled && nrf52Bluetooth) {
|
||||
@@ -1605,8 +1605,8 @@ void disableBluetooth()
|
||||
{
|
||||
#if HAS_BLUETOOTH
|
||||
#ifdef ARCH_ESP32
|
||||
if (nimbleBluetooth)
|
||||
nimbleBluetooth->deinit();
|
||||
if (bluetoothApi)
|
||||
bluetoothApi->deinit();
|
||||
#elif defined(ARCH_NRF52)
|
||||
if (nrf52Bluetooth)
|
||||
nrf52Bluetooth->shutdown();
|
||||
|
||||
@@ -0,0 +1,493 @@
|
||||
#include "HopScalingModule.h"
|
||||
#include "SafeFile.h"
|
||||
#include "meshUtils.h"
|
||||
|
||||
#if HAS_VARIABLE_HOPS
|
||||
|
||||
#include "FSCommon.h"
|
||||
#include "NodeDB.h"
|
||||
#include "SPILock.h"
|
||||
#include "concurrency/LockGuard.h"
|
||||
#include "mesh-pb-constants.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
namespace
|
||||
{
|
||||
// Module scheduling
|
||||
constexpr uint32_t INITIAL_DELAY_MS = 30 * 1000UL; // Startup grace period before first run
|
||||
constexpr uint32_t RUN_INTERVAL_MS = 5 * 60 * 1000UL; // Emit micro-summary every 5 minutes
|
||||
// RUNS_PER_HOUR is a public class constant in HopScalingModule.h
|
||||
|
||||
// Persistence
|
||||
// Note: this only needs incrementing if the published arrangement changes. For testing purposes, or prior to widespread release,
|
||||
// it can stay the same even if the internal layout changes.
|
||||
constexpr uint32_t HISTOGRAM_STATE_MAGIC = 0x48535432; // 'HST2' — layout v2
|
||||
constexpr uint8_t HISTOGRAM_STATE_VERSION = 1;
|
||||
constexpr const char *HISTOGRAM_STATE_FILE = "/prefs/hopScalingState.bin";
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct PersistedHistogram {
|
||||
uint32_t magic;
|
||||
uint8_t version;
|
||||
uint8_t samplingDenominator;
|
||||
uint8_t filteringDenominator;
|
||||
uint8_t filterDenomHoldRollsRemaining; // rollHour() calls remaining in the hold; 0 when expired/not active
|
||||
uint16_t hashSeed;
|
||||
Record entries[HopScalingModule::CAPACITY]; // full 512-byte array; count derived on load
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
} // namespace
|
||||
|
||||
HopScalingModule *hopScalingModule;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lifecycle
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
HopScalingModule::HopScalingModule() : concurrency::OSThread("HopScaling")
|
||||
{
|
||||
clear();
|
||||
loadFromDisk();
|
||||
setIntervalFromNow(INITIAL_DELAY_MS);
|
||||
}
|
||||
|
||||
void HopScalingModule::clear()
|
||||
{
|
||||
memset(entries, 0, sizeof(entries));
|
||||
count = 0;
|
||||
samplingDenominator = DENOM_MIN;
|
||||
filteringDenominator = DENOM_MIN;
|
||||
filteringDenomHoldRollsRemaining = 0;
|
||||
lastPerHopCounts = {};
|
||||
lastSuggestedHop = MAX_HOP;
|
||||
lastPoliteNumer = POLITENESS_DEFAULT;
|
||||
lastTrendStats = {};
|
||||
memset(denominatorHistory, DENOM_MIN, sizeof(denominatorHistory));
|
||||
#ifndef PIO_UNIT_TESTING
|
||||
hashSeed = static_cast<uint16_t>(random());
|
||||
#else
|
||||
hashSeed = 0; // deterministic in unit tests
|
||||
#endif
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Persistence
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void HopScalingModule::saveToDisk() const
|
||||
{
|
||||
#ifdef FSCom
|
||||
FSCom.mkdir("/prefs");
|
||||
PersistedHistogram state{};
|
||||
state.magic = HISTOGRAM_STATE_MAGIC;
|
||||
state.version = HISTOGRAM_STATE_VERSION;
|
||||
state.samplingDenominator = samplingDenominator;
|
||||
state.filteringDenominator = filteringDenominator;
|
||||
state.filterDenomHoldRollsRemaining = filteringDenomHoldRollsRemaining;
|
||||
state.hashSeed = hashSeed;
|
||||
// Save all CAPACITY slots; count is reconstructed on load by scanning seenHoursAgo.
|
||||
memcpy(state.entries, entries, sizeof(state.entries));
|
||||
auto file = SafeFile(HISTOGRAM_STATE_FILE, true);
|
||||
const size_t written = file.write(reinterpret_cast<const uint8_t *>(&state), sizeof(state));
|
||||
if (file.close() && written == sizeof(state)) {
|
||||
LOG_DEBUG("[HOPSCALE] Saved: count=%u samp=1/%u filt=1/%u holdRollsRemaining=%u", count, samplingDenominator,
|
||||
filteringDenominator, state.filterDenomHoldRollsRemaining);
|
||||
} else {
|
||||
LOG_WARN("[HOPSCALE] Failed to write %s (%u of %u bytes)", HISTOGRAM_STATE_FILE, static_cast<unsigned>(written),
|
||||
static_cast<unsigned>(sizeof(state)));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void HopScalingModule::loadFromDisk()
|
||||
{
|
||||
#ifdef FSCom
|
||||
concurrency::LockGuard g(spiLock);
|
||||
auto file = FSCom.open(HISTOGRAM_STATE_FILE, FILE_O_READ);
|
||||
if (!file)
|
||||
return;
|
||||
PersistedHistogram state{};
|
||||
const bool readOk = (file.read(reinterpret_cast<uint8_t *>(&state), sizeof(state)) == sizeof(state));
|
||||
file.close();
|
||||
// Validate magic, version, denom range, denom power-of-two invariant, and hold counter.
|
||||
if (!readOk || state.magic != HISTOGRAM_STATE_MAGIC || state.version != HISTOGRAM_STATE_VERSION ||
|
||||
state.samplingDenominator < DENOM_MIN || state.samplingDenominator > DENOM_MAX ||
|
||||
state.filteringDenominator < state.samplingDenominator || state.filteringDenominator > DENOM_MAX ||
|
||||
!is_pow_of_2(state.samplingDenominator) || !is_pow_of_2(state.filteringDenominator) ||
|
||||
state.filterDenomHoldRollsRemaining > FILTER_DENOM_HOLD_ROLLS) {
|
||||
LOG_DEBUG("[HOPSCALE] No valid persisted state (magic=%08x ver=%u samp=%u filt=%u hold=%u), starting fresh", state.magic,
|
||||
state.version, state.samplingDenominator, state.filteringDenominator, state.filterDenomHoldRollsRemaining);
|
||||
return;
|
||||
}
|
||||
// Derive count by scanning: active entries have seenHoursAgo != 0; pack them to the front.
|
||||
|
||||
uint8_t restored = 0;
|
||||
for (uint8_t i = 0; i < CAPACITY && restored < CAPACITY; i++) {
|
||||
if (state.entries[i].seenHoursAgo != 0u) {
|
||||
entries[restored++] = state.entries[i];
|
||||
}
|
||||
}
|
||||
count = restored;
|
||||
samplingDenominator = state.samplingDenominator;
|
||||
filteringDenominator = state.filteringDenominator;
|
||||
filteringDenomHoldRollsRemaining = state.filterDenomHoldRollsRemaining;
|
||||
// denominatorHistory can't be recovered; initialise all slots to filteringDenominator so
|
||||
// the first few post-reboot scaledPerHour values use a safe (slightly conservative) multiplier.
|
||||
memset(denominatorHistory, filteringDenominator, sizeof(denominatorHistory));
|
||||
hashSeed = state.hashSeed;
|
||||
LOG_INFO("[HOPSCALE] Restored: count=%u samp=1/%u filt=1/%u holdRollsRemaining=%u", count, samplingDenominator,
|
||||
filteringDenominator, state.filterDenomHoldRollsRemaining);
|
||||
#endif
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Core API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void HopScalingModule::samplePacketForHistogram(uint32_t nodeId, uint8_t hopCount)
|
||||
{
|
||||
const uint16_t hash = hashNodeId(nodeId);
|
||||
|
||||
if (!passesFilter(hash, samplingDenominator))
|
||||
return;
|
||||
|
||||
hopCount = std::min(hopCount, MAX_HOP);
|
||||
|
||||
// Update an existing entry
|
||||
Record *entry = nullptr;
|
||||
for (uint8_t i = 0; i < count; i++) {
|
||||
if (entries[i].nodeHash == hash) {
|
||||
entry = &entries[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (entry) {
|
||||
entry->hops_away = hopCount;
|
||||
markCurrentHour(*entry);
|
||||
return;
|
||||
}
|
||||
|
||||
// New node: trim if necessary before allocating a slot
|
||||
if (getFillPercentage() >= FILL_HIGH_PCT) {
|
||||
trimIfNeeded();
|
||||
}
|
||||
|
||||
if (count < CAPACITY) {
|
||||
entries[count].nodeHash = hash;
|
||||
entries[count].hops_away = hopCount;
|
||||
entries[count].seenHoursAgo = 1u; // mark current hour
|
||||
count++;
|
||||
} else {
|
||||
LOG_WARN("[HOPSCALE] Histogram full at samp=1/%u (DENOM_MAX=%u); dropping node hash=0x%04x; hop recommendation may be "
|
||||
"skewed!!!",
|
||||
samplingDenominator, DENOM_MAX, hash);
|
||||
}
|
||||
}
|
||||
|
||||
void HopScalingModule::rollHour()
|
||||
{
|
||||
// Advance denominatorHistory before the tally so each slot h holds the filteringDenominator
|
||||
// that was active when seenHoursAgo bit h was set. hourlyRaw[h] is then gated per-slot by
|
||||
// denominatorHistory[h], giving a correct population estimate for each historical hour even
|
||||
// when filteringDenominator changes between rolls. Scale-up backfills the entire array so
|
||||
// the invariant holds retroactively (see trimIfNeeded()).
|
||||
for (uint8_t h = 12; h > 0; h--)
|
||||
denominatorHistory[h] = denominatorHistory[h - 1];
|
||||
denominatorHistory[0] = filteringDenominator;
|
||||
|
||||
// 1. Tally per-hop counts and per-slot hourly activity in one pass.
|
||||
// hourlyRaw[h]: gated per-slot by denominatorHistory[h] so the raw count and its
|
||||
// multiplier are always consistent, even across filteringDenominator transitions.
|
||||
// counts.*: gated uniformly by the current filteringDenominator for a consistent
|
||||
// population estimate used by the hop-walk recommendation (step 2).
|
||||
PerHopCounts counts{};
|
||||
uint16_t hourlyRaw[13] = {};
|
||||
uint16_t trendNewThisHour = 0;
|
||||
uint16_t trendReturning = 0;
|
||||
uint16_t trendLapsed = 0;
|
||||
uint16_t trendOlderThan4h = 0;
|
||||
uint16_t trendAgingOut = 0;
|
||||
for (uint8_t i = 0; i < count; i++) {
|
||||
const uint16_t hash = entries[i].nodeHash;
|
||||
const uint32_t seen = entries[i].seenHoursAgo;
|
||||
|
||||
// Per-slot hourly activity: gate each slot by its own denominator.
|
||||
for (uint8_t h = 0; h < 13; h++) {
|
||||
if ((seen & (1u << h)) && passesFilter(hash, denominatorHistory[h]))
|
||||
hourlyRaw[h]++;
|
||||
}
|
||||
|
||||
// Hop counts and trend stats: uniform current-denominator gate.
|
||||
if (!passesFilter(hash, filteringDenominator))
|
||||
continue;
|
||||
|
||||
if (seenInLast13h(entries[i])) {
|
||||
counts.perHop[entries[i].hops_away]++;
|
||||
counts.total++;
|
||||
}
|
||||
const bool heardThisHour = (seen & 1u) != 0u;
|
||||
const bool heardLastHour = (seen & 2u) != 0u;
|
||||
const bool hasOlderHistory = (seen >> 1u) != 0u;
|
||||
const bool recentlySilent = (seen & 0xFu) == 0u;
|
||||
if (heardThisHour && !hasOlderHistory)
|
||||
trendNewThisHour++;
|
||||
else if (heardThisHour && hasOlderHistory)
|
||||
trendReturning++;
|
||||
if (!heardThisHour && heardLastHour)
|
||||
trendLapsed++;
|
||||
if (recentlySilent && (seen & 0x1FF0u) != 0u)
|
||||
trendOlderThan4h++;
|
||||
if (seen == (1u << 12u))
|
||||
trendAgingOut++;
|
||||
}
|
||||
lastPerHopCounts = counts;
|
||||
|
||||
// 1b. Compute politeness factor from the 0-2 h vs 1-3 h activity ratio.
|
||||
{
|
||||
const uint32_t recent = static_cast<uint32_t>(hourlyRaw[0]) + hourlyRaw[1];
|
||||
const uint32_t older = static_cast<uint32_t>(hourlyRaw[1]) + hourlyRaw[2];
|
||||
if (older > 1 && recent > 1) {
|
||||
const uint32_t r = static_cast<uint32_t>(recent) * ACTIVITY_WEIGHT_SCALE;
|
||||
const uint32_t o = static_cast<uint32_t>(older);
|
||||
if (r < o * ACTIVITY_WEIGHT_GENEROUS_MAX_NUMER)
|
||||
lastPoliteNumer = POLITENESS_GENEROUS;
|
||||
else if (r > o * ACTIVITY_WEIGHT_STRICT_MIN_NUMER)
|
||||
lastPoliteNumer = POLITENESS_STRICT;
|
||||
else
|
||||
lastPoliteNumer = POLITENESS_DEFAULT;
|
||||
} else {
|
||||
lastPoliteNumer = POLITENESS_DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
// 1c. Scale and cache trend stats (denominatorHistory already advanced above).
|
||||
{
|
||||
MeshTrendStats t{};
|
||||
for (uint8_t h = 0; h < 13; h++) {
|
||||
const uint32_t s = static_cast<uint32_t>(hourlyRaw[h]) * denominatorHistory[h];
|
||||
t.scaledPerHour[h] = static_cast<uint16_t>(std::min<uint32_t>(s, UINT16_MAX));
|
||||
}
|
||||
auto scale = [&](uint16_t raw) -> uint16_t {
|
||||
return static_cast<uint16_t>(std::min<uint32_t>(static_cast<uint32_t>(raw) * filteringDenominator, UINT16_MAX));
|
||||
};
|
||||
t.newThisHour = scale(trendNewThisHour);
|
||||
t.returningThisHour = scale(trendReturning);
|
||||
t.lapsedSinceLastHour = scale(trendLapsed);
|
||||
t.olderThan4h = scale(trendOlderThan4h);
|
||||
t.agingOut = scale(trendAgingOut);
|
||||
lastTrendStats = t;
|
||||
}
|
||||
|
||||
// 2. Walk scaled hop buckets to produce a hop-limit recommendation.
|
||||
// effectiveMin: walk threshold — first hop whose cumulative count reaches this.
|
||||
// effectiveMax: ceiling on the one-hop extension check with GENEROUS politeness.
|
||||
const uint16_t effectiveMin = TARGET_AFFECTED_NODES;
|
||||
const uint16_t effectiveMax = MAX_TARGET_NODES;
|
||||
uint8_t suggested = MAX_HOP;
|
||||
if (counts.total > 0) {
|
||||
uint32_t cumulative = 0;
|
||||
for (uint8_t hop = 0; hop <= MAX_HOP; hop++) {
|
||||
cumulative += static_cast<uint32_t>(counts.perHop[hop]) * filteringDenominator;
|
||||
if (cumulative >= effectiveMin) {
|
||||
suggested = hop;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (suggested < MAX_HOP) {
|
||||
const uint32_t atNext = static_cast<uint32_t>(counts.perHop[suggested + 1]) * filteringDenominator;
|
||||
// politeLimit = effectiveMin + gap * politeNumer / POLITENESS_DENOM
|
||||
// Multiply both sides by POLITENESS_DENOM to stay in integers.
|
||||
const uint32_t gap = static_cast<uint32_t>(effectiveMax) - static_cast<uint32_t>(effectiveMin);
|
||||
if ((cumulative + atNext) * POLITENESS_DENOM <=
|
||||
static_cast<uint32_t>(effectiveMin) * POLITENESS_DENOM + gap * lastPoliteNumer) {
|
||||
suggested++;
|
||||
}
|
||||
}
|
||||
}
|
||||
lastSuggestedHop = suggested;
|
||||
|
||||
// 3. Log scaled per-hop counts and recommendation.
|
||||
{
|
||||
uint16_t scaled[MAX_HOP + 1];
|
||||
for (uint8_t h = 0; h <= MAX_HOP; h++) {
|
||||
const uint32_t s = static_cast<uint32_t>(counts.perHop[h]) * filteringDenominator;
|
||||
scaled[h] = static_cast<uint16_t>(std::min<uint32_t>(s, UINT16_MAX));
|
||||
}
|
||||
const uint32_t scaledTotal = static_cast<uint32_t>(counts.total) * filteringDenominator;
|
||||
memcpy(lastScaledPerHop, scaled, sizeof(lastScaledPerHop));
|
||||
LOG_INFO("[HOPSCALE] rollHour: entries=%u/128 samp=1/%u filt=1/%u counted=%u est=%u suggestedHop=%u polite=%u/4", count,
|
||||
samplingDenominator, filteringDenominator, counts.total, static_cast<unsigned>(scaledTotal), suggested,
|
||||
lastPoliteNumer);
|
||||
|
||||
const auto &ts = lastTrendStats;
|
||||
LOG_INFO("[HOPSCALE] scaledSeenPerHour (h0=now): [%u %u %u %u %u %u %u %u %u %u %u %u %u]", ts.scaledPerHour[0],
|
||||
ts.scaledPerHour[1], ts.scaledPerHour[2], ts.scaledPerHour[3], ts.scaledPerHour[4], ts.scaledPerHour[5],
|
||||
ts.scaledPerHour[6], ts.scaledPerHour[7], ts.scaledPerHour[8], ts.scaledPerHour[9], ts.scaledPerHour[10],
|
||||
ts.scaledPerHour[11], ts.scaledPerHour[12]);
|
||||
LOG_INFO("[HOPSCALE] trend: new=%u returning=%u lapsed=%u olderThan4h=%u agingOut=%u", ts.newThisHour,
|
||||
ts.returningThisHour, ts.lapsedSinceLastHour, ts.olderThan4h, ts.agingOut);
|
||||
}
|
||||
|
||||
// 4. Scale-down check: if fewer than FILL_LOW_PCT% of capacity pass the filteringDenominator
|
||||
// gate and are active, halve samplingDenominator to admit more nodes.
|
||||
// Note: during a filteringDenominator hold period, lowering samplingDenominator does not
|
||||
// immediately improve counts.total (new admissions don't pass the elevated
|
||||
// filteringDenominator). On a genuinely quieting mesh this check can therefore fire on
|
||||
// consecutive hours, cascading samplingDenominator toward DENOM_MIN. This is intentional:
|
||||
// rapid re-admission allows quick recovery if the mesh returns. The hop recommendation
|
||||
// stays conservative (MAX_HOP) throughout because filteringDenominator remains elevated;
|
||||
// step 5 below re-synchronises the denominators once the hold expires.
|
||||
if (counts.total * 100u < static_cast<uint32_t>(CAPACITY) * FILL_LOW_PCT) {
|
||||
if (samplingDenominator > DENOM_MIN) {
|
||||
samplingDenominator = static_cast<uint8_t>(samplingDenominator / 2u);
|
||||
LOG_INFO("[HOPSCALE] Scale-down: sampling denom halved to %u (filter denom=%u)", samplingDenominator,
|
||||
filteringDenominator);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Tick down the hold counter; once it reaches zero, halve filteringDenominator toward
|
||||
// samplingDenominator once per rollHour() (= once per hour) rather than a single jump:
|
||||
// avoids a sudden large change in the hop-walk count when samplingDenominator cascaded
|
||||
// down significantly during the hold period. No new hold is placed on each step — the
|
||||
// 13-roll hold already guaranteed that re-admitted nodes have full seenHoursAgo history;
|
||||
// further pacing is provided naturally by the 1-step-per-hour rate. denominatorHistory
|
||||
// is updated automatically by the shift at the top of rollHour(), so no backfill here.
|
||||
if (filteringDenominator > samplingDenominator) {
|
||||
if (filteringDenomHoldRollsRemaining > 0)
|
||||
filteringDenomHoldRollsRemaining--;
|
||||
if (filteringDenomHoldRollsRemaining == 0) {
|
||||
const uint8_t stepped = static_cast<uint8_t>(filteringDenominator / 2u);
|
||||
filteringDenominator = (stepped > samplingDenominator) ? stepped : samplingDenominator;
|
||||
LOG_INFO("[HOPSCALE] Filter denom stepped to %u (samp=1/%u)", filteringDenominator, samplingDenominator);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Shift all seen bitmaps left by one slot (opens a fresh slot for the new hour).
|
||||
for (uint8_t i = 0; i < count; i++) {
|
||||
rollSeenBits(entries[i]);
|
||||
}
|
||||
|
||||
if (histogramRollCount < 255)
|
||||
histogramRollCount++;
|
||||
|
||||
saveToDisk();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void HopScalingModule::trimIfNeeded()
|
||||
{
|
||||
// Step 1: evict stale entries (not seen in any of the past 13 hours).
|
||||
uint8_t newCount = 0;
|
||||
for (uint8_t i = 0; i < count; i++) {
|
||||
if (seenInLast13h(entries[i])) {
|
||||
if (i != newCount) {
|
||||
entries[newCount] = entries[i];
|
||||
}
|
||||
newCount++;
|
||||
}
|
||||
}
|
||||
count = newCount;
|
||||
|
||||
// Step 2: if still too full, double the sampling denominator and remove non-matching entries.
|
||||
if (getFillPercentage() >= FILL_HIGH_PCT && samplingDenominator < DENOM_MAX) {
|
||||
samplingDenominator = static_cast<uint8_t>(
|
||||
std::min<uint16_t>(static_cast<uint16_t>(samplingDenominator) * 2u, static_cast<uint16_t>(DENOM_MAX)));
|
||||
filteringDenominator = std::max(filteringDenominator, samplingDenominator);
|
||||
filteringDenomHoldRollsRemaining = FILTER_DENOM_HOLD_ROLLS;
|
||||
// Raise any denominatorHistory slot that is below the new filteringDenominator.
|
||||
// Slots already above it (recorded during a prior scale-up that hasn't fully stepped
|
||||
// down yet) are left untouched: eviction at samplingDenominator retains exactly those
|
||||
// entries, so the old higher gate remains accurate for those historical hours.
|
||||
// Slots below the new value must be raised because the eviction removed entries that
|
||||
// had been admitted at the looser old gate — the remaining entries represent a 1/N
|
||||
// subsample where N is the new filteringDenominator, not the old smaller value.
|
||||
for (uint8_t h = 0; h < 13; h++)
|
||||
denominatorHistory[h] = std::max(denominatorHistory[h], filteringDenominator);
|
||||
LOG_INFO("[HOPSCALE] Scale-up: samp denom doubled to %u (filt=%u)", samplingDenominator, filteringDenominator);
|
||||
|
||||
newCount = 0;
|
||||
for (uint8_t i = 0; i < count; i++) {
|
||||
if (passesFilter(entries[i].nodeHash, samplingDenominator)) {
|
||||
if (i != newCount) {
|
||||
entries[newCount] = entries[i];
|
||||
}
|
||||
newCount++;
|
||||
}
|
||||
}
|
||||
count = newCount;
|
||||
}
|
||||
}
|
||||
|
||||
void HopScalingModule::logStatusReport(bool didHourlyUpdate) const
|
||||
{
|
||||
const bool histActive = (histogramRollCount > 0 && count > 0);
|
||||
const auto &histCounts = lastPerHopCounts;
|
||||
const uint8_t runsRemaining = didHourlyUpdate ? RUNS_PER_HOUR : (RUNS_PER_HOUR - runsSinceLastHourlyUpdate);
|
||||
const uint8_t minsUntilRollover = runsRemaining * (RUN_INTERVAL_MS / (60 * 1000UL));
|
||||
|
||||
LOG_INFO("[HOPSCALE] hop=%u histActive=%u fill=%u%% samp=1/%u filt=1/%u entries=%u lastCounted=%u polite=%u/4 "
|
||||
"nextRoll=%umin",
|
||||
lastRequiredHop, histActive ? 1u : 0u, getFillPercentage(), samplingDenominator, filteringDenominator, count,
|
||||
histCounts.total, lastPoliteNumer, minsUntilRollover);
|
||||
|
||||
LOG_INFO("[HOPSCALE] nodes perHop: [%u %u %u %u %u %u %u %u]", histCounts.perHop[0], histCounts.perHop[1],
|
||||
histCounts.perHop[2], histCounts.perHop[3], histCounts.perHop[4], histCounts.perHop[5], histCounts.perHop[6],
|
||||
histCounts.perHop[7]);
|
||||
LOG_INFO("[HOPSCALE] last scaled perHop: [%u %u %u %u %u %u %u %u]", lastScaledPerHop[0], lastScaledPerHop[1],
|
||||
lastScaledPerHop[2], lastScaledPerHop[3], lastScaledPerHop[4], lastScaledPerHop[5], lastScaledPerHop[6],
|
||||
lastScaledPerHop[7]);
|
||||
}
|
||||
|
||||
int32_t HopScalingModule::runOnce()
|
||||
{
|
||||
const bool isFirstRun = !hasCompletedInitialRun;
|
||||
bool didHourlyUpdate = false;
|
||||
|
||||
if (isFirstRun) {
|
||||
hasCompletedInitialRun = true;
|
||||
runsSinceLastHourlyUpdate = 0;
|
||||
didHourlyUpdate = true;
|
||||
} else {
|
||||
runsSinceLastHourlyUpdate++;
|
||||
if (runsSinceLastHourlyUpdate >= RUNS_PER_HOUR) {
|
||||
runsSinceLastHourlyUpdate = 0;
|
||||
didHourlyUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (didHourlyUpdate && !isFirstRun) {
|
||||
rollHour();
|
||||
}
|
||||
|
||||
if (didHourlyUpdate) {
|
||||
uint8_t suggested = (histogramRollCount > 0 && count > 0) ? lastSuggestedHop : HOP_MAX;
|
||||
// Role-based hop floor: TRACKER/TAK_TRACKER always reach at least 2 hops,
|
||||
// SENSOR reaches at least 1, so these reporting roles remain reachable even
|
||||
// on a dense mesh where the histogram recommends a lower hop count.
|
||||
uint8_t roleFloor = 0;
|
||||
switch (config.device.role) {
|
||||
case meshtastic_Config_DeviceConfig_Role_TRACKER:
|
||||
case meshtastic_Config_DeviceConfig_Role_TAK_TRACKER:
|
||||
roleFloor = 2;
|
||||
break;
|
||||
case meshtastic_Config_DeviceConfig_Role_SENSOR:
|
||||
roleFloor = 1;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
lastRequiredHop = std::max(suggested, roleFloor);
|
||||
}
|
||||
|
||||
logStatusReport(didHourlyUpdate);
|
||||
|
||||
return RUN_INTERVAL_MS;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,351 @@
|
||||
#pragma once
|
||||
|
||||
#include "MeshTypes.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "configuration.h"
|
||||
#include "mesh/Default.h"
|
||||
#include "mesh/mesh-pb-constants.h"
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#if HAS_VARIABLE_HOPS
|
||||
|
||||
/**
|
||||
* HopScalingModule: Sampled hop-distance histogram for mesh-aware hop limit recommendations.
|
||||
*
|
||||
* Memory layout: 512 bytes total (128 entries × 4 bytes/entry, no padding)
|
||||
* - 16-bit XOR-fold hash of node ID
|
||||
* - 3-bit hops away (0–7)
|
||||
* - 13-bit hourly seen bitmap
|
||||
* All three fields are packed into a single 32-bit Record; sizeof(Record) == 4.
|
||||
*
|
||||
* Sampling:
|
||||
* - A node is added only when passesFilter(hashNodeId(nodeId), samplingDenominator),
|
||||
* i.e. (hash16(nodeId) & (samplingDenominator – 1)) == 0 (hash-space subsample, not raw ID)
|
||||
* - samplingDenominator starts at 1 (sample all), doubles when the list exceeds FILL_HIGH_PCT
|
||||
* - filteringDenominator tracks samplingDenominator upward immediately but does not drop back
|
||||
* down until FILTER_DENOM_HOLD_MS (13 h) have elapsed since the last scale-up
|
||||
*
|
||||
* Hourly rollover (rollHour()):
|
||||
* - Summarises per-hop node counts for entries matching filteringDenominator and seen in the
|
||||
* last 13 hours
|
||||
* - Scales each hop bucket by filteringDenominator and walks the buckets to recommend a hop
|
||||
* limit, matching the same algorithm used in HopScalingModule
|
||||
* - Shifts the 13-bit seen bitmap left by one slot to open a fresh slot for the new hour;
|
||||
* nodes not seen in 13 consecutive hours have all seen bits cleared (stale)
|
||||
* - Checks for scale-down: if fewer than FILL_LOW_PCT of capacity pass filteringDenominator,
|
||||
* samplingDenominator is halved (filteringDenominator is held until the 13-h lock expires)
|
||||
*
|
||||
* Thread-safety: all access is single-threaded via the main loop cooperative scheduler.
|
||||
*/
|
||||
|
||||
struct Record {
|
||||
uint32_t nodeHash : 16;
|
||||
uint32_t hops_away : 3;
|
||||
uint32_t seenHoursAgo : 13;
|
||||
};
|
||||
static_assert(sizeof(Record) == 4);
|
||||
|
||||
class HopScalingModule : private concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
// -----------------------------------------------------------------------
|
||||
// Capacity and memory layout
|
||||
// -----------------------------------------------------------------------
|
||||
static constexpr size_t CAPACITY = 128;
|
||||
static constexpr size_t ENTRY_BYTES = sizeof(Record);
|
||||
static constexpr size_t TOTAL_BYTES = CAPACITY * ENTRY_BYTES;
|
||||
|
||||
// Denominator limits (must be powers of 2)
|
||||
static constexpr uint8_t DENOM_MIN = 1;
|
||||
static constexpr uint8_t DENOM_MAX = 128;
|
||||
|
||||
static constexpr uint8_t MAX_HOP = 7;
|
||||
|
||||
// Fill-level thresholds (percent of CAPACITY)
|
||||
static constexpr uint8_t FILL_HIGH_PCT = 80;
|
||||
static constexpr uint8_t FILL_LOW_PCT = 20;
|
||||
|
||||
// How long filteringDenominator is held at an elevated level before it may drop.
|
||||
//
|
||||
// This value is deliberately equal to the seenHoursAgo window (13 hours / 13 bits).
|
||||
// Invariant: every entry that existed when a scale-up fired had seenHoursAgo != 0 at
|
||||
// that moment (trimIfNeeded() evicts stale entries before doubling the denominator),
|
||||
// so it remains seenInLast13h for at most 13 more rollHour() calls — exactly the
|
||||
// hold duration. That means entries from the scale-up event keep counts.total above
|
||||
// the scale-down threshold for the entire hold period under normal (active) mesh
|
||||
// conditions. On a genuinely quieting mesh the scale-down CAN fire before the hold
|
||||
// expires — each firing halves samplingDenominator but filteringDenominator stays
|
||||
// elevated, so the hop recommendation correctly stays conservative (MAX_HOP) while
|
||||
// the cascade runs. The cascade is bounded at DENOM_MIN (7 halvings from DENOM_MAX);
|
||||
// when the hold finally expires, step 5 of rollHour() halves filteringDenominator
|
||||
// once per hour (rather than jumping directly to samplingDenominator) until the two
|
||||
// converge, giving the hop-walk a gradual, 1-step-per-hour descent.
|
||||
static constexpr uint32_t FILTER_DENOM_HOLD_MS = 13UL * 60UL * 60UL * 1000UL; // 13 h (documentation only)
|
||||
// Number of rollHour() calls the hold spans — equals the seenHoursAgo window width.
|
||||
// filteringDenomHoldRollsRemaining is initialised to this value on scale-up and
|
||||
// decremented once per rollHour(); step-down begins when it reaches zero.
|
||||
static constexpr uint8_t FILTER_DENOM_HOLD_ROLLS = 13u;
|
||||
|
||||
// Hop-walk: target cumulative affected-node count when choosing a hop limit
|
||||
static constexpr uint16_t TARGET_AFFECTED_NODES = default_hop_scaling_min_target_nodes;
|
||||
|
||||
// Clamp bounds enforced on min_target_nodes / max_target_nodes
|
||||
static constexpr uint16_t MIN_TARGET_NODES_FLOOR = default_hop_scaling_min_target_nodes_floor;
|
||||
static constexpr uint16_t MAX_TARGET_NODES_CEILING = default_hop_scaling_max_target_nodes_ceiling;
|
||||
static constexpr uint16_t MAX_TARGET_NODES = default_hop_scaling_max_target_nodes;
|
||||
|
||||
// Politeness factors for the one-hop extension check in the hop walk.
|
||||
// Stored as integer numerators over POLITENESS_DENOM (4):
|
||||
// politeLimit = min + gap * politeNumer / POLITENESS_DENOM
|
||||
// STRICT → min + 25% of gap; DEFAULT → midpoint; GENEROUS → max
|
||||
static constexpr uint8_t POLITENESS_DENOM = 4u;
|
||||
static constexpr uint8_t POLITENESS_GENEROUS = 4u; // 4/4 = 1.00
|
||||
static constexpr uint8_t POLITENESS_DEFAULT = 2u; // 2/4 = 0.50
|
||||
static constexpr uint8_t POLITENESS_STRICT = 1u; // 1/4 = 0.25
|
||||
|
||||
// Activity weight thresholds (ratio of 0-2 h window vs 1-3 h window).
|
||||
// Cross-multiply form: recent * ACTIVITY_WEIGHT_SCALE vs older * threshold_numer.
|
||||
// GENEROUS if recent*10 < older*9 (ratio < 0.9); STRICT if recent*10 > older*12 (ratio > 1.2)
|
||||
static constexpr uint8_t ACTIVITY_WEIGHT_SCALE = 10u;
|
||||
static constexpr uint8_t ACTIVITY_WEIGHT_GENEROUS_MAX_NUMER = 9u;
|
||||
static constexpr uint8_t ACTIVITY_WEIGHT_STRICT_MIN_NUMER = 12u;
|
||||
|
||||
// Scheduling: number of 5-minute runOnce() ticks that make up one hourly rollover
|
||||
static constexpr uint8_t RUNS_PER_HOUR = 12;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Types
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// Per-hop node counts produced at each hourly rollover.
|
||||
struct PerHopCounts {
|
||||
uint16_t perHop[MAX_HOP + 1] = {};
|
||||
uint16_t total = 0;
|
||||
};
|
||||
|
||||
/// Mesh activity trend stats produced at each hourly rollover.
|
||||
/// All counts are scaled by filteringDenominator (i.e. estimated full-mesh population).
|
||||
///
|
||||
/// Bitmap interpretation (before the hourly shift): bit 0 = just-completed hour, bit 12 = 12 h ago.
|
||||
struct MeshTrendStats {
|
||||
/// Estimated node count per hour slot (h=0 is the just-completed hour, h=12 is 12 h ago).
|
||||
uint16_t scaledPerHour[13] = {};
|
||||
/// Nodes heard only this hour with no prior bitmap history — indicates new arrivals.
|
||||
uint16_t newThisHour = 0;
|
||||
/// Nodes heard this hour that also appeared in at least one older hour — stable regulars.
|
||||
uint16_t returningThisHour = 0;
|
||||
/// Nodes heard last hour but silent this hour — potential departures.
|
||||
uint16_t lapsedSinceLastHour = 0;
|
||||
/// Nodes absent from the last 4 hours but still present in some older hour (5–13 h) — quieting down.
|
||||
uint16_t olderThan4h = 0;
|
||||
/// Nodes whose only remaining history is the 13th hour (bit 12 only) — about to age out entirely.
|
||||
uint16_t agingOut = 0;
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Lifecycle
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
HopScalingModule();
|
||||
~HopScalingModule() = default;
|
||||
|
||||
/// Reset all entries and state.
|
||||
void clear();
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Core API
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// Record a received packet.
|
||||
/// Adds or updates an entry when passesFilter(hashNodeId(nodeId), samplingDenominator),
|
||||
/// i.e. when the 16-bit XOR-fold hash of the node ID falls in the 1/samplingDenominator
|
||||
/// subsample of the hash space. This is NOT a raw nodeId modulo check.
|
||||
/// Marks the current hour as seen and updates the stored hop count to the last observed value.
|
||||
/// Triggers a trim pass if the list exceeds FILL_HIGH_PCT after the insertion.
|
||||
void samplePacketForHistogram(uint32_t nodeId, uint8_t hopCount);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Accessors
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
uint8_t getLastRequiredHop() const { return lastRequiredHop; }
|
||||
uint8_t getEntryCount() const { return count; }
|
||||
uint8_t getFillPercentage() const { return static_cast<uint8_t>((static_cast<uint16_t>(count) * 100u) / CAPACITY); }
|
||||
uint8_t getSamplingDenominator() const { return samplingDenominator; }
|
||||
uint8_t getFilteringDenominator() const { return filteringDenominator; }
|
||||
float getPoliteness() const { return lastPoliteNumer / static_cast<float>(POLITENESS_DENOM); }
|
||||
const PerHopCounts &getLastPerHopCounts() const { return lastPerHopCounts; }
|
||||
uint8_t getLastSuggestedHop() const { return lastSuggestedHop; }
|
||||
const MeshTrendStats &getLastTrendStats() const { return lastTrendStats; }
|
||||
|
||||
// Compatibility accessors used by tests
|
||||
uint8_t getCompactHistogramEntryCount() const { return getEntryCount(); }
|
||||
uint8_t getCompactHistogramDenominator() const { return getSamplingDenominator(); }
|
||||
uint8_t getCompactHistogramFilterDenominator() const { return getFilteringDenominator(); }
|
||||
uint8_t getCompactHistogramSuggestedHop() const { return getLastSuggestedHop(); }
|
||||
size_t getCompactHistogramAllSampleCount() const { return getEntryCount(); }
|
||||
|
||||
/// Force both sampling and filtering denominators to a specific value.
|
||||
/// Intended for unit tests that need a deterministic starting denominator.
|
||||
void setSamplingDenominator(uint8_t d)
|
||||
{
|
||||
samplingDenominator = (d < DENOM_MIN) ? DENOM_MIN : (d > DENOM_MAX ? DENOM_MAX : d);
|
||||
filteringDenominator = samplingDenominator;
|
||||
filteringDenomHoldRollsRemaining = 0;
|
||||
}
|
||||
|
||||
#ifdef PIO_UNIT_TESTING
|
||||
// Writable from tests as HopScalingModule::s_testNowMs; drives nowMs() in PIO_UNIT_TESTING builds.
|
||||
inline static uint32_t s_testNowMs = 0;
|
||||
/// Override the per-session hash seed. Use in tests that need a specific sampling distribution.
|
||||
void setHashSeed(uint16_t seed) { hashSeed = seed; }
|
||||
uint16_t getHashSeed() const { return hashSeed; }
|
||||
/// Expose hashNodeId for tests that need to compute which node IDs pass a given denominator.
|
||||
uint16_t hashNodeIdPublic(uint32_t nodeId) const { return hashNodeId(nodeId); }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
int32_t runOnce() override;
|
||||
|
||||
private:
|
||||
#ifdef PIO_UNIT_TESTING
|
||||
friend class HopScalingTestShim;
|
||||
#endif
|
||||
|
||||
/// Perform hourly rollover.
|
||||
/// 1. Tallies per-hop counts for entries matching filteringDenominator and seen in 13 h.
|
||||
/// 2. Walks the scaled hop buckets and returns the recommended hop limit.
|
||||
/// 3. Logs scaled per-hop counts and recommendation.
|
||||
/// 4. Checks for scale-down (< FILL_LOW_PCT of capacity pass filteringDenominator).
|
||||
/// 5. Decrements filteringDenomHoldRollsRemaining (if > 0); once it reaches zero, halves
|
||||
/// filteringDenominator once toward samplingDenominator per rollHour() call.
|
||||
/// 6. Shifts all seen bitmaps left by one hour slot.
|
||||
void rollHour();
|
||||
// -----------------------------------------------------------------------
|
||||
// Persistence
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// Persist the histogram state (entries, denominators, hold-timer) to flash.
|
||||
/// No-op on platforms without a filesystem. Performs a full delete-and-rewrite of
|
||||
/// the state file on each call; avoid calling more frequently than once per rollHour().
|
||||
void saveToDisk() const;
|
||||
|
||||
/// Restore histogram state from flash. Safe to call even when no file exists.
|
||||
/// Call once after construction, before the first rollHour(), to warm-start the
|
||||
/// histogram across reboots without waiting 13 hours for data to re-accumulate.
|
||||
/// The restored entries are available immediately for sampling, but the first
|
||||
/// rollHour() (triggered by the second runOnce() tick) is needed before a warm-start
|
||||
/// recommendation replaces the HOP_MAX boot default.
|
||||
void loadFromDisk();
|
||||
|
||||
/// Remove stale entries (seen-bits all zero) and, if the list is still crowded,
|
||||
/// double samplingDenominator and filteringDenominator and remove non-matching entries.
|
||||
void trimIfNeeded();
|
||||
|
||||
void logStatusReport(bool didHourlyUpdate) const;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Histogram storage
|
||||
// -----------------------------------------------------------------------
|
||||
Record entries[CAPACITY] = {};
|
||||
uint8_t count = 0;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Denominator state
|
||||
//
|
||||
// Two separate denominators control two distinct gates:
|
||||
//
|
||||
// samplingDenominator — admission gate. A node is added/updated only when
|
||||
// passesFilter(hash, samplingDenominator). Lower value = more permissive =
|
||||
// more nodes enter = represents recent mesh state.
|
||||
//
|
||||
// filteringDenominator — counting gate. The hop-walk tally in rollHour() only
|
||||
// counts entries that pass passesFilter(hash, filteringDenominator). It moves
|
||||
// up with samplingDenominator immediately (scale-up) but is held at the
|
||||
// elevated value for FILTER_DENOM_HOLD_MS (13 h) after any scale-up before it
|
||||
// may drop back down (scale-down).
|
||||
//
|
||||
// Why the estimate is invariant: passesFilter uses a hash-based uniform subsample.
|
||||
// For any two powers-of-two denominators D ≤ F, the fraction of D-sampled entries
|
||||
// that also pass F is exactly D/F. Therefore:
|
||||
// raw_count × F = (total × D/F) × F = total × D
|
||||
// The population estimate is the same whether we count with D or with F.
|
||||
// The hold period is not about accuracy — it is about stability: it prevents the
|
||||
// hop recommendation from reacting to recently-admitted nodes that have not yet
|
||||
// accumulated enough seenHoursAgo history to be statistically reliable.
|
||||
//
|
||||
// denominatorHistory[h] — the filteringDenominator used to both gate and scale
|
||||
// hourlyRaw[h]. Invariant: denominatorHistory[h] always equals the
|
||||
// filteringDenominator that was active when seenHoursAgo bit h was set.
|
||||
// rollHour() advances the array at the very start (before the tally loop), then
|
||||
// gates hourlyRaw[h] per-slot by denominatorHistory[h] — each slot's raw count
|
||||
// and multiplier are therefore always consistent, even when filteringDenominator
|
||||
// changes between rolls (e.g. hold expiry). On scale-up (trimIfNeeded()), the
|
||||
// entire array is backfilled uniformly with the new filteringDenominator to
|
||||
// preserve the invariant retroactively for all 13 slots. Initialised to
|
||||
// DENOM_MIN (1); scaledPerHour slots that draw from a 1 entry are unscaled —
|
||||
// correct for a fresh instance with no prior history.
|
||||
// -----------------------------------------------------------------------
|
||||
uint8_t samplingDenominator = DENOM_MIN;
|
||||
uint8_t filteringDenominator = DENOM_MIN;
|
||||
uint8_t filteringDenomHoldRollsRemaining = 0; // counts down from FILTER_DENOM_HOLD_ROLLS to 0; step-down fires at 0
|
||||
uint8_t denominatorHistory[13] = {};
|
||||
uint16_t hashSeed = 0;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Cached hourly results
|
||||
// -----------------------------------------------------------------------
|
||||
PerHopCounts lastPerHopCounts = {};
|
||||
uint16_t lastScaledPerHop[MAX_HOP + 1] = {};
|
||||
uint8_t lastSuggestedHop = MAX_HOP;
|
||||
uint8_t lastPoliteNumer = POLITENESS_DEFAULT;
|
||||
MeshTrendStats lastTrendStats = {};
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Hop recommendation state
|
||||
// -----------------------------------------------------------------------
|
||||
uint8_t lastRequiredHop = HOP_MAX;
|
||||
uint8_t histogramRollCount = 0;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Scheduler state
|
||||
// -----------------------------------------------------------------------
|
||||
bool hasCompletedInitialRun = false;
|
||||
uint8_t runsSinceLastHourlyUpdate = 0;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Inline record helpers
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
// Record field semantics:
|
||||
// nodeHash → XOR-fold of full 32-bit node ID to 16 bits
|
||||
// hops_away → hop distance (0–7)
|
||||
// seenHoursAgo → 13-bit per-hour seen bitmap
|
||||
// bit 0 = seen in the current / most-recent hour
|
||||
// bit 12 = seen 12 hours ago
|
||||
// Shifts left on each rollHour(); 0 means not seen in 13 h.
|
||||
|
||||
/// XOR-fold + golden-ratio hash of a 32-bit node ID to 16 bits, mixed with the session seed.
|
||||
/// Multiplying by floor(2^32 / φ) gives uniform avalanche; XORing the seed ensures different
|
||||
/// devices (or the same device after a clear()) sample a different subset of node IDs.
|
||||
/// For seed=0 the function is deterministic, which is used in PIO_UNIT_TESTING builds.
|
||||
uint16_t hashNodeId(uint32_t nodeId) const { return static_cast<uint16_t>((nodeId * 2654435761u) >> 16) ^ hashSeed; }
|
||||
static bool seenInLast13h(const Record &r) { return r.seenHoursAgo != 0u; }
|
||||
static void markCurrentHour(Record &r) { r.seenHoursAgo |= 1u; }
|
||||
static void rollSeenBits(Record &r) { r.seenHoursAgo = (r.seenHoursAgo << 1u) & 0x1FFFu; }
|
||||
static bool passesFilter(uint16_t nodeHash, uint8_t denom) { return (nodeHash & static_cast<uint16_t>(denom - 1u)) == 0u; }
|
||||
|
||||
public:
|
||||
// Clock — public so tests can share the same timebase via HopScalingModule::s_testNowMs
|
||||
#ifdef PIO_UNIT_TESTING
|
||||
static uint32_t nowMs() { return s_testNowMs; }
|
||||
#else
|
||||
static uint32_t nowMs() { return millis(); }
|
||||
#endif
|
||||
};
|
||||
|
||||
extern HopScalingModule *hopScalingModule;
|
||||
|
||||
#endif
|
||||
@@ -41,6 +41,9 @@
|
||||
#if HAS_TRAFFIC_MANAGEMENT && !MESHTASTIC_EXCLUDE_TRAFFIC_MANAGEMENT
|
||||
#include "modules/TrafficManagementModule.h"
|
||||
#endif
|
||||
#if HAS_VARIABLE_HOPS
|
||||
#include "modules/HopScalingModule.h"
|
||||
#endif
|
||||
#include "modules/TextMessageModule.h"
|
||||
#if !MESHTASTIC_EXCLUDE_TRACEROUTE
|
||||
#include "modules/TraceRouteModule.h"
|
||||
@@ -131,6 +134,10 @@ void setupModules()
|
||||
}
|
||||
#endif
|
||||
|
||||
#if HAS_VARIABLE_HOPS
|
||||
hopScalingModule = new HopScalingModule();
|
||||
#endif
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ADMIN
|
||||
adminModule = new AdminModule();
|
||||
#endif
|
||||
|
||||
@@ -94,7 +94,7 @@ bool SerialModule::isValidConfig(const meshtastic_ModuleConfig_SerialConfig &con
|
||||
const char *warning =
|
||||
"Invalid Serial config: override console serial port is only supported in NMEA and CalTopo output-only modes.";
|
||||
LOG_ERROR(warning);
|
||||
#if !IS_RUNNING_TESTS
|
||||
#ifndef PIO_UNIT_TESTING
|
||||
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
||||
cn->level = meshtastic_LogRecord_Level_ERROR;
|
||||
cn->time = getValidTime(RTCQualityFromNet);
|
||||
|
||||
+4
-170
@@ -23,10 +23,6 @@
|
||||
#include <ETH.h>
|
||||
#endif // HAS_ETHERNET
|
||||
#include "Default.h"
|
||||
#if !defined(ARCH_NRF52) || NRF52_USE_JSON
|
||||
#include "serialization/JSON.h"
|
||||
#include "serialization/MeshPacketSerializer.h"
|
||||
#endif
|
||||
#include <Throttle.h>
|
||||
#include <assert.h>
|
||||
#include <utility>
|
||||
@@ -147,96 +143,6 @@ inline void onReceiveProto(char *topic, byte *payload, size_t length)
|
||||
router->enqueueReceivedMessage(p.release());
|
||||
}
|
||||
|
||||
#if !defined(ARCH_NRF52) || NRF52_USE_JSON
|
||||
// returns true if this is a valid JSON envelope which we accept on downlink
|
||||
inline bool isValidJsonEnvelope(JSONObject &json)
|
||||
{
|
||||
// Generate node ID from nodenum for comparison
|
||||
std::string nodeId = nodeDB->getNodeId();
|
||||
// if "sender" is provided, avoid processing packets we uplinked
|
||||
return (json.find("sender") != json.end() ? (json["sender"]->AsString().compare(nodeId) != 0) : true) &&
|
||||
(json.find("hopLimit") != json.end() ? json["hopLimit"]->IsNumber() : true) && // hop limit should be a number
|
||||
(json.find("from") != json.end()) && json["from"]->IsNumber() &&
|
||||
(json["from"]->AsNumber() == nodeDB->getNodeNum()) && // only accept message if the "from" is us
|
||||
(json.find("type") != json.end()) && json["type"]->IsString() && // should specify a type
|
||||
(json.find("payload") != json.end()); // should have a payload
|
||||
}
|
||||
|
||||
inline void onReceiveJson(byte *payload, size_t length)
|
||||
{
|
||||
char payloadStr[length + 1];
|
||||
memcpy(payloadStr, payload, length);
|
||||
payloadStr[length] = 0; // null terminated string
|
||||
std::unique_ptr<JSONValue> json_value(JSON::Parse(payloadStr));
|
||||
if (json_value == nullptr) {
|
||||
LOG_ERROR("JSON received payload on MQTT but not a valid JSON");
|
||||
return;
|
||||
}
|
||||
|
||||
JSONObject json;
|
||||
json = json_value->AsObject();
|
||||
|
||||
if (!isValidJsonEnvelope(json)) {
|
||||
LOG_ERROR("JSON received payload on MQTT but not a valid envelope");
|
||||
return;
|
||||
}
|
||||
|
||||
// this is a valid envelope
|
||||
if (json["type"]->AsString().compare("sendtext") == 0 && json["payload"]->IsString()) {
|
||||
std::string jsonPayloadStr = json["payload"]->AsString();
|
||||
LOG_INFO("JSON payload %s, length %u", jsonPayloadStr.c_str(), jsonPayloadStr.length());
|
||||
|
||||
// construct protobuf data packet using TEXT_MESSAGE, send it to the mesh
|
||||
meshtastic_MeshPacket *p = router->allocForSending();
|
||||
p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
||||
if (json.find("channel") != json.end() && json["channel"]->IsNumber() &&
|
||||
(json["channel"]->AsNumber() < channels.getNumChannels()))
|
||||
p->channel = json["channel"]->AsNumber();
|
||||
if (json.find("to") != json.end() && json["to"]->IsNumber())
|
||||
p->to = json["to"]->AsNumber();
|
||||
if (json.find("hopLimit") != json.end() && json["hopLimit"]->IsNumber())
|
||||
p->hop_limit = json["hopLimit"]->AsNumber();
|
||||
if (jsonPayloadStr.length() <= sizeof(p->decoded.payload.bytes)) {
|
||||
memcpy(p->decoded.payload.bytes, jsonPayloadStr.c_str(), jsonPayloadStr.length());
|
||||
p->decoded.payload.size = jsonPayloadStr.length();
|
||||
service->sendToMesh(p, RX_SRC_LOCAL);
|
||||
} else {
|
||||
LOG_WARN("Received MQTT json payload too long, drop");
|
||||
}
|
||||
} else if (json["type"]->AsString().compare("sendposition") == 0 && json["payload"]->IsObject()) {
|
||||
// invent the "sendposition" type for a valid envelope
|
||||
JSONObject posit;
|
||||
posit = json["payload"]->AsObject(); // get nested JSON Position
|
||||
meshtastic_Position pos = meshtastic_Position_init_default;
|
||||
if (posit.find("latitude_i") != posit.end() && posit["latitude_i"]->IsNumber())
|
||||
pos.latitude_i = posit["latitude_i"]->AsNumber();
|
||||
if (posit.find("longitude_i") != posit.end() && posit["longitude_i"]->IsNumber())
|
||||
pos.longitude_i = posit["longitude_i"]->AsNumber();
|
||||
if (posit.find("altitude") != posit.end() && posit["altitude"]->IsNumber())
|
||||
pos.altitude = posit["altitude"]->AsNumber();
|
||||
if (posit.find("time") != posit.end() && posit["time"]->IsNumber())
|
||||
pos.time = posit["time"]->AsNumber();
|
||||
|
||||
// construct protobuf data packet using POSITION, send it to the mesh
|
||||
meshtastic_MeshPacket *p = router->allocForSending();
|
||||
p->decoded.portnum = meshtastic_PortNum_POSITION_APP;
|
||||
if (json.find("channel") != json.end() && json["channel"]->IsNumber() &&
|
||||
(json["channel"]->AsNumber() < channels.getNumChannels()))
|
||||
p->channel = json["channel"]->AsNumber();
|
||||
if (json.find("to") != json.end() && json["to"]->IsNumber())
|
||||
p->to = json["to"]->AsNumber();
|
||||
if (json.find("hopLimit") != json.end() && json["hopLimit"]->IsNumber())
|
||||
p->hop_limit = json["hopLimit"]->AsNumber();
|
||||
p->decoded.payload.size =
|
||||
pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_Position_msg,
|
||||
&pos); // make the Data protobuf from position
|
||||
service->sendToMesh(p, RX_SRC_LOCAL);
|
||||
} else {
|
||||
LOG_DEBUG("JSON ignore downlink message with unsupported type");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Determines if the given IPAddress is a private IPv4 address, i.e. not routable on the public internet.
|
||||
bool isPrivateIpAddress(const IPAddress &ip)
|
||||
{
|
||||
@@ -386,26 +292,6 @@ void MQTT::onReceive(char *topic, byte *payload, size_t length)
|
||||
return;
|
||||
}
|
||||
|
||||
// check if this is a json payload message by comparing the topic start
|
||||
if (moduleConfig.mqtt.json_enabled && (strncmp(topic, jsonTopic.c_str(), jsonTopic.length()) == 0)) {
|
||||
#if !defined(ARCH_NRF52) || NRF52_USE_JSON
|
||||
// parse the channel name from the topic string
|
||||
// the topic has been checked above for having jsonTopic prefix, so just move past it
|
||||
char *channelName = topic + jsonTopic.length();
|
||||
// if another "/" was added, parse string up to that character
|
||||
channelName = strtok(channelName, "/") ? strtok(channelName, "/") : channelName;
|
||||
// We allow downlink JSON packets only on a channel named "mqtt"
|
||||
const meshtastic_Channel &sendChannel = channels.getByName(channelName);
|
||||
if (!(strncasecmp(channels.getGlobalId(sendChannel.index), Channels::mqttChannel, strlen(Channels::mqttChannel)) == 0 &&
|
||||
sendChannel.settings.downlink_enabled)) {
|
||||
LOG_WARN("JSON downlink received on channel not called 'mqtt' or without downlink enabled");
|
||||
return;
|
||||
}
|
||||
onReceiveJson(payload, length);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
onReceiveProto(topic, payload, length);
|
||||
}
|
||||
|
||||
@@ -433,12 +319,10 @@ MQTT::MQTT() : concurrency::OSThread("mqtt"), mqttQueue(MAX_MQTT_QUEUE)
|
||||
|
||||
if (*moduleConfig.mqtt.root) {
|
||||
cryptTopic = moduleConfig.mqtt.root + cryptTopic;
|
||||
jsonTopic = moduleConfig.mqtt.root + jsonTopic;
|
||||
mapTopic = moduleConfig.mqtt.root + mapTopic;
|
||||
isConfiguredForDefaultRootTopic = isDefaultRootTopic(moduleConfig.mqtt.root);
|
||||
} else {
|
||||
cryptTopic = "msh" + cryptTopic;
|
||||
jsonTopic = "msh" + jsonTopic;
|
||||
mapTopic = "msh" + mapTopic;
|
||||
isConfiguredForDefaultRootTopic = true;
|
||||
}
|
||||
@@ -466,7 +350,7 @@ MQTT::MQTT() : concurrency::OSThread("mqtt"), mqttQueue(MAX_MQTT_QUEUE)
|
||||
enabled = true;
|
||||
runASAP = true;
|
||||
reconnectCount = 0;
|
||||
#if !IS_RUNNING_TESTS
|
||||
#ifndef PIO_UNIT_TESTING
|
||||
publishNodeInfo();
|
||||
#endif
|
||||
}
|
||||
@@ -589,14 +473,6 @@ void MQTT::sendSubscriptions()
|
||||
std::string topic = cryptTopic + channels.getGlobalId(i) + "/+";
|
||||
LOG_INFO("Subscribe to %s", topic.c_str());
|
||||
pubSub.subscribe(topic.c_str(), 1); // FIXME, is QOS 1 right?
|
||||
#if !defined(ARCH_NRF52) || \
|
||||
defined(NRF52_USE_JSON) // JSON is not supported on nRF52, see issue #2804 ### Fixed by using ArduinoJSON ###
|
||||
if (moduleConfig.mqtt.json_enabled == true) {
|
||||
std::string topicDecoded = jsonTopic + channels.getGlobalId(i) + "/+";
|
||||
LOG_INFO("Subscribe to %s", topicDecoded.c_str());
|
||||
pubSub.subscribe(topicDecoded.c_str(), 1); // FIXME, is QOS 1 right?
|
||||
}
|
||||
#endif // ARCH_NRF52 NRF52_USE_JSON
|
||||
}
|
||||
}
|
||||
#if !MESHTASTIC_EXCLUDE_PKI
|
||||
@@ -674,7 +550,7 @@ bool MQTT::isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTC
|
||||
const char *warning = "Could not reach the MQTT server. Settings will be saved, but please verify the server "
|
||||
"address and credentials.";
|
||||
LOG_WARN(warning);
|
||||
#if !IS_RUNNING_TESTS
|
||||
#ifndef PIO_UNIT_TESTING
|
||||
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
||||
if (cn) {
|
||||
cn->level = meshtastic_LogRecord_Level_WARNING;
|
||||
@@ -690,7 +566,7 @@ bool MQTT::isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTC
|
||||
#else
|
||||
const char *warning = "Invalid MQTT config: proxy_to_client_enabled must be enabled on nodes that do not have a network";
|
||||
LOG_ERROR(warning);
|
||||
#if !IS_RUNNING_TESTS
|
||||
#ifndef PIO_UNIT_TESTING
|
||||
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
||||
cn->level = meshtastic_LogRecord_Level_ERROR;
|
||||
cn->time = getValidTime(RTCQualityFromNet);
|
||||
@@ -706,7 +582,7 @@ bool MQTT::isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTC
|
||||
if (defaultServer && !IS_ONE_OF(parsed.serverPort, PubSubConfig::defaultPort, PubSubConfig::defaultPortTls)) {
|
||||
const char *warning = "Invalid MQTT config: default server address must not have a port specified";
|
||||
LOG_ERROR(warning);
|
||||
#if !IS_RUNNING_TESTS
|
||||
#ifndef PIO_UNIT_TESTING
|
||||
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
||||
cn->level = meshtastic_LogRecord_Level_ERROR;
|
||||
cn->time = getValidTime(RTCQualityFromNet);
|
||||
@@ -735,33 +611,6 @@ void MQTT::publishQueuedMessages()
|
||||
const std::unique_ptr<QueueEntry> entry(mqttQueue.dequeuePtr(0));
|
||||
LOG_INFO("publish %s, %u bytes from queue", entry->topic.c_str(), entry->envBytes.size());
|
||||
publish(entry->topic.c_str(), entry->envBytes.data(), entry->envBytes.size(), false);
|
||||
|
||||
#if !defined(ARCH_NRF52) || \
|
||||
defined(NRF52_USE_JSON) // JSON is not supported on nRF52, see issue #2804 ### Fixed by using ArduinoJson ###
|
||||
if (!moduleConfig.mqtt.json_enabled)
|
||||
return;
|
||||
|
||||
// handle json topic
|
||||
const DecodedServiceEnvelope env(entry->envBytes.data(), entry->envBytes.size());
|
||||
if (!env.validDecode || env.packet == NULL || env.channel_id == NULL)
|
||||
return;
|
||||
|
||||
auto jsonString = MeshPacketSerializer::JsonSerialize(env.packet);
|
||||
if (jsonString.length() == 0)
|
||||
return;
|
||||
|
||||
// Generate node ID from nodenum for topic
|
||||
std::string nodeId = nodeDB->getNodeId();
|
||||
|
||||
std::string topicJson;
|
||||
if (env.packet->pki_encrypted) {
|
||||
topicJson = jsonTopic + "PKI/" + nodeId;
|
||||
} else {
|
||||
topicJson = jsonTopic + env.channel_id + "/" + nodeId;
|
||||
}
|
||||
LOG_INFO("JSON publish message to %s, %u bytes: %s", topicJson.c_str(), jsonString.length(), jsonString.c_str());
|
||||
publish(topicJson.c_str(), jsonString.c_str(), false);
|
||||
#endif // ARCH_NRF52 NRF52_USE_JSON
|
||||
}
|
||||
|
||||
void MQTT::onSend(const meshtastic_MeshPacket &mp_encrypted, const meshtastic_MeshPacket &mp_decoded, ChannelIndex chIndex)
|
||||
@@ -825,21 +674,6 @@ void MQTT::onSend(const meshtastic_MeshPacket &mp_encrypted, const meshtastic_Me
|
||||
if (moduleConfig.mqtt.proxy_to_client_enabled || this->isConnectedDirectly()) {
|
||||
LOG_DEBUG("MQTT Publish %s, %u bytes", topic.c_str(), numBytes);
|
||||
publish(topic.c_str(), bytes, numBytes, false);
|
||||
|
||||
#if !defined(ARCH_NRF52) || \
|
||||
defined(NRF52_USE_JSON) // JSON is not supported on nRF52, see issue #2804 ### Fixed by using ArduinoJson ###
|
||||
if (!moduleConfig.mqtt.json_enabled)
|
||||
return;
|
||||
// handle json topic
|
||||
auto jsonString = MeshPacketSerializer::JsonSerialize(&mp_decoded);
|
||||
if (jsonString.length() == 0)
|
||||
return;
|
||||
// Generate node ID from nodenum for JSON topic
|
||||
std::string nodeIdForJson = nodeDB->getNodeId();
|
||||
std::string topicJson = jsonTopic + channelId + "/" + nodeIdForJson;
|
||||
LOG_INFO("JSON publish message to %s, %u bytes: %s", topicJson.c_str(), jsonString.length(), jsonString.c_str());
|
||||
publish(topicJson.c_str(), jsonString.c_str(), false);
|
||||
#endif // ARCH_NRF52 NRF52_USE_JSON
|
||||
} else {
|
||||
LOG_INFO("MQTT not connected, queue packet");
|
||||
QueueEntry *entry;
|
||||
|
||||
+2
-6
@@ -6,9 +6,6 @@
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "mesh/Channels.h"
|
||||
#include "mesh/generated/meshtastic/mqtt.pb.h"
|
||||
#if !defined(ARCH_NRF52) || NRF52_USE_JSON
|
||||
#include "serialization/JSON.h"
|
||||
#endif
|
||||
#if HAS_WIFI
|
||||
#include <WiFiClient.h>
|
||||
#if __has_include(<WiFiClientSecure.h>)
|
||||
@@ -101,9 +98,8 @@ class MQTT : private concurrency::OSThread
|
||||
explicit MQTT(std::unique_ptr<MQTTClient> mqttClient);
|
||||
#endif
|
||||
|
||||
std::string cryptTopic = "/2/e/"; // msh/2/e/CHANNELID/NODEID
|
||||
std::string jsonTopic = "/2/json/"; // msh/2/json/CHANNELID/NODEID
|
||||
std::string mapTopic = "/2/map/"; // For protobuf-encoded MapReport messages
|
||||
std::string cryptTopic = "/2/e/"; // msh/2/e/CHANNELID/NODEID
|
||||
std::string mapTopic = "/2/map/"; // For protobuf-encoded MapReport messages
|
||||
|
||||
// For map reporting (only applies when enabled)
|
||||
const uint32_t default_map_position_precision = 14; // defaults to max. offset of ~1459m
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "configuration.h"
|
||||
#if !MESHTASTIC_EXCLUDE_BLUETOOTH
|
||||
#if !MESHTASTIC_EXCLUDE_BLUETOOTH && !defined(CONFIG_IDF_TARGET_ESP32P4)
|
||||
#include "BluetoothCommon.h"
|
||||
#include "NimbleBluetooth.h"
|
||||
#include "PowerFSM.h"
|
||||
@@ -25,6 +25,9 @@
|
||||
|
||||
namespace
|
||||
{
|
||||
// Maintainer note: this backend intentionally diverges from HostedBluetooth in a few platform-specific areas.
|
||||
// If you change shared BLE flow here (PhoneAPI queue/sync, security/pairing, mesh GATT/advertising,
|
||||
// connect/disconnect handling), review and update HostedBluetooth.cpp as needed.
|
||||
constexpr uint16_t kPreferredBleMtu = 517;
|
||||
constexpr uint16_t kPreferredBleTxOctets = 251;
|
||||
constexpr uint16_t kPreferredBleTxTimeUs = (kPreferredBleTxOctets + 14) * 8;
|
||||
@@ -568,7 +571,7 @@ class NimbleBluetoothSecurityCallback : public BLESecurityCallbacks
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->setFont(FONT_MEDIUM);
|
||||
display->drawString(x_offset + x, y_offset + y, "Bluetooth");
|
||||
#if !defined(OLED_TINY)
|
||||
#if !defined(OLED_TINY) && !defined(M5STACK_UNITC6L)
|
||||
display->setFont(FONT_SMALL);
|
||||
y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5;
|
||||
display->drawString(x_offset + x, y_offset + y, "Enter this code");
|
||||
@@ -739,6 +742,19 @@ int NimbleBluetooth::getRssi()
|
||||
return 0; // No active BLE connection
|
||||
}
|
||||
|
||||
uint16_t connHandle = nimbleBluetoothConnHandle.load();
|
||||
|
||||
if (connHandle == BLE_HS_CONN_HANDLE_NONE) {
|
||||
const auto peers = bleServer->getPeerDevices(true);
|
||||
if (!peers.empty()) {
|
||||
connHandle = peers.begin()->first;
|
||||
nimbleBluetoothConnHandle = connHandle;
|
||||
}
|
||||
}
|
||||
|
||||
if (connHandle == BLE_HS_CONN_HANDLE_NONE) {
|
||||
return 0; // Connection handle not available yet
|
||||
}
|
||||
int8_t rssi = 0;
|
||||
const int rc = ble_gap_conn_rssi(conn_handle, &rssi);
|
||||
|
||||
@@ -862,7 +878,7 @@ void NimbleBluetooth::setupService()
|
||||
/// Given a level between 0-100, update the BLE attribute
|
||||
void updateBatteryLevel(uint8_t level)
|
||||
{
|
||||
if ((config.bluetooth.enabled == true) && nimbleBluetooth && nimbleBluetooth->isConnected()) {
|
||||
if ((config.bluetooth.enabled == true) && BatteryCharacteristic && nimbleBluetooth && nimbleBluetooth->isConnected()) {
|
||||
BatteryCharacteristic->setValue(&level, 1);
|
||||
BatteryCharacteristic->notify();
|
||||
}
|
||||
@@ -892,4 +908,13 @@ void clearNVS()
|
||||
ESP.restart();
|
||||
#endif
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
void updateBatteryLevel(uint8_t level)
|
||||
{
|
||||
(void)level;
|
||||
}
|
||||
|
||||
void clearNVS() {}
|
||||
#endif
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
#pragma once
|
||||
#include "BluetoothCommon.h"
|
||||
|
||||
class NimbleBluetooth : BluetoothApi
|
||||
class NimbleBluetooth : public BluetoothApi
|
||||
{
|
||||
public:
|
||||
void setup();
|
||||
void shutdown();
|
||||
void deinit();
|
||||
void clearBonds();
|
||||
bool isActive();
|
||||
bool isConnected();
|
||||
int getRssi();
|
||||
void sendLog(const uint8_t *logMessage, size_t length);
|
||||
void setup() override;
|
||||
void shutdown() override;
|
||||
void deinit() override;
|
||||
void clearBonds() override;
|
||||
bool isActive() override;
|
||||
bool isConnected() override;
|
||||
int getRssi() override;
|
||||
void sendLog(const uint8_t *logMessage, size_t length) override;
|
||||
void startAdvertising();
|
||||
virtual ~NimbleBluetooth() {}
|
||||
bool isDeInit = false;
|
||||
|
||||
private:
|
||||
|
||||
@@ -4,9 +4,13 @@
|
||||
#include "esp_task_wdt.h"
|
||||
#include "main.h"
|
||||
|
||||
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !MESHTASTIC_EXCLUDE_BLUETOOTH
|
||||
#if !MESHTASTIC_EXCLUDE_BLUETOOTH
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32P4)
|
||||
#include "bluetooth/HostedBluetooth.h"
|
||||
#elif !defined(CONFIG_IDF_TARGET_ESP32S2)
|
||||
#include "nimble/NimbleBluetooth.h"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#include <MeshtasticOTA.h>
|
||||
|
||||
@@ -40,16 +44,30 @@ void setBluetoothEnable(bool enable)
|
||||
if (config.bluetooth.enabled == true)
|
||||
#endif
|
||||
{
|
||||
if (!nimbleBluetooth) {
|
||||
nimbleBluetooth = new NimbleBluetooth();
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32P4)
|
||||
if (!enable) {
|
||||
if (bluetoothApi && bluetoothApi->isActive()) {
|
||||
bluetoothApi->shutdown();
|
||||
powerMon->clearState(meshtastic_PowerMon_State_BT_On);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (enable && !nimbleBluetooth->isActive()) {
|
||||
#endif
|
||||
|
||||
if (!bluetoothApi) {
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32P4)
|
||||
bluetoothApi = new HostedBluetooth();
|
||||
#else
|
||||
bluetoothApi = new NimbleBluetooth();
|
||||
#endif
|
||||
}
|
||||
if (enable && !bluetoothApi->isActive()) {
|
||||
powerMon->setState(meshtastic_PowerMon_State_BT_On);
|
||||
nimbleBluetooth->setup();
|
||||
bluetoothApi->setup();
|
||||
}
|
||||
// For ESP32, no way to recover from bluetooth shutdown without reboot
|
||||
// BLE advertising automatically stops when MCU enters light-sleep(?)
|
||||
// For deep-sleep, shutdown hardware with nimbleBluetooth->deinit(). Requires reboot to reverse
|
||||
// For deep-sleep, shutdown hardware with bluetoothApi->deinit(). Requires reboot to reverse
|
||||
}
|
||||
}
|
||||
#else
|
||||
|
||||
@@ -562,7 +562,7 @@ void portduinoSetup()
|
||||
}
|
||||
|
||||
getMacAddr(dmac);
|
||||
#ifndef UNIT_TEST
|
||||
#ifndef PIO_UNIT_TESTING
|
||||
if (dmac[0] == 0 && dmac[1] == 0 && dmac[2] == 0 && dmac[3] == 0 && dmac[4] == 0 && dmac[5] == 0) {
|
||||
std::cout << "*** Blank MAC Address not allowed!" << std::endl;
|
||||
std::cout << "Please set a MAC Address in config.yaml using either MACAddress or MACAddressSource." << std::endl;
|
||||
|
||||
@@ -1,245 +0,0 @@
|
||||
/*
|
||||
* File JSON.cpp part of the SimpleJSON Library - http://mjpa.in/json
|
||||
*
|
||||
* Copyright (C) 2010 Mike Anchor
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "JSON.h"
|
||||
|
||||
/**
|
||||
* Blocks off the public constructor
|
||||
*
|
||||
* @access private
|
||||
*
|
||||
*/
|
||||
JSON::JSON() {}
|
||||
|
||||
/**
|
||||
* Parses a complete JSON encoded string
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param char* data The JSON text
|
||||
*
|
||||
* @return JSONValue* Returns a JSON Value representing the root, or NULL on error
|
||||
*/
|
||||
JSONValue *JSON::Parse(const char *data)
|
||||
{
|
||||
// Skip any preceding whitespace, end of data = no JSON = fail
|
||||
if (!SkipWhitespace(&data))
|
||||
return NULL;
|
||||
|
||||
// We need the start of a value here now...
|
||||
JSONValue *value = JSONValue::Parse(&data);
|
||||
if (value == NULL)
|
||||
return NULL;
|
||||
|
||||
// Can be white space now and should be at the end of the string then...
|
||||
if (SkipWhitespace(&data)) {
|
||||
delete value;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// We're now at the end of the string
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns the passed in JSONValue into a JSON encode string
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param JSONValue* value The root value
|
||||
*
|
||||
* @return std::string Returns a JSON encoded string representation of the given value
|
||||
*/
|
||||
std::string JSON::Stringify(const JSONValue *value)
|
||||
{
|
||||
if (value != NULL)
|
||||
return value->Stringify();
|
||||
else
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips over any whitespace characters (space, tab, \r or \n) defined by the JSON spec
|
||||
*
|
||||
* @access protected
|
||||
*
|
||||
* @param char** data Pointer to a char* that contains the JSON text
|
||||
*
|
||||
* @return bool Returns true if there is more data, or false if the end of the text was reached
|
||||
*/
|
||||
bool JSON::SkipWhitespace(const char **data)
|
||||
{
|
||||
while (**data != 0 && (**data == ' ' || **data == '\t' || **data == '\r' || **data == '\n'))
|
||||
(*data)++;
|
||||
|
||||
return **data != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a JSON String as defined by the spec - "<some chars>"
|
||||
* Any escaped characters are swapped out for their unescaped values
|
||||
*
|
||||
* @access protected
|
||||
*
|
||||
* @param char** data Pointer to a char* that contains the JSON text
|
||||
* @param std::string& str Reference to a std::string to receive the extracted string
|
||||
*
|
||||
* @return bool Returns true on success, false on failure
|
||||
*/
|
||||
bool JSON::ExtractString(const char **data, std::string &str)
|
||||
{
|
||||
str = "";
|
||||
|
||||
while (**data != 0) {
|
||||
// Save the char so we can change it if need be
|
||||
char next_char = **data;
|
||||
|
||||
// Escaping something?
|
||||
if (next_char == '\\') {
|
||||
// Move over the escape char
|
||||
(*data)++;
|
||||
|
||||
// Deal with the escaped char
|
||||
switch (**data) {
|
||||
case '"':
|
||||
next_char = '"';
|
||||
break;
|
||||
case '\\':
|
||||
next_char = '\\';
|
||||
break;
|
||||
case '/':
|
||||
next_char = '/';
|
||||
break;
|
||||
case 'b':
|
||||
next_char = '\b';
|
||||
break;
|
||||
case 'f':
|
||||
next_char = '\f';
|
||||
break;
|
||||
case 'n':
|
||||
next_char = '\n';
|
||||
break;
|
||||
case 'r':
|
||||
next_char = '\r';
|
||||
break;
|
||||
case 't':
|
||||
next_char = '\t';
|
||||
break;
|
||||
case 'u': {
|
||||
// We need 5 chars (4 hex + the 'u') or its not valid
|
||||
if (!simplejson_csnlen(*data, 5))
|
||||
return false;
|
||||
|
||||
// Deal with the chars
|
||||
next_char = 0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
// Do it first to move off the 'u' and leave us on the
|
||||
// final hex digit as we move on by one later on
|
||||
(*data)++;
|
||||
|
||||
next_char <<= 4;
|
||||
|
||||
// Parse the hex digit
|
||||
if (**data >= '0' && **data <= '9')
|
||||
next_char |= (**data - '0');
|
||||
else if (**data >= 'A' && **data <= 'F')
|
||||
next_char |= (10 + (**data - 'A'));
|
||||
else if (**data >= 'a' && **data <= 'f')
|
||||
next_char |= (10 + (**data - 'a'));
|
||||
else {
|
||||
// Invalid hex digit = invalid JSON
|
||||
return false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// By the spec, only the above cases are allowed
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// End of the string?
|
||||
else if (next_char == '"') {
|
||||
(*data)++;
|
||||
str.shrink_to_fit(); // Remove unused capacity
|
||||
return true;
|
||||
}
|
||||
|
||||
// Disallowed char?
|
||||
else if (next_char < ' ' && next_char != '\t') {
|
||||
// SPEC Violation: Allow tabs due to real world cases
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add the next char
|
||||
str += next_char;
|
||||
|
||||
// Move on
|
||||
(*data)++;
|
||||
}
|
||||
|
||||
// If we're here, the string ended incorrectly
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses some text as though it is an integer
|
||||
*
|
||||
* @access protected
|
||||
*
|
||||
* @param char** data Pointer to a char* that contains the JSON text
|
||||
*
|
||||
* @return double Returns the double value of the number found
|
||||
*/
|
||||
double JSON::ParseInt(const char **data)
|
||||
{
|
||||
double integer = 0;
|
||||
while (**data != 0 && **data >= '0' && **data <= '9')
|
||||
integer = integer * 10 + (*(*data)++ - '0');
|
||||
|
||||
return integer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses some text as though it is a decimal
|
||||
*
|
||||
* @access protected
|
||||
*
|
||||
* @param char** data Pointer to a char* that contains the JSON text
|
||||
*
|
||||
* @return double Returns the double value of the decimal found
|
||||
*/
|
||||
double JSON::ParseDecimal(const char **data)
|
||||
{
|
||||
double decimal = 0.0;
|
||||
double factor = 0.1;
|
||||
while (**data != 0 && **data >= '0' && **data <= '9') {
|
||||
int digit = (*(*data)++ - '0');
|
||||
decimal = decimal + digit * factor;
|
||||
factor *= 0.1;
|
||||
}
|
||||
return decimal;
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
* File JSON.h part of the SimpleJSON Library - http://mjpa.in/json
|
||||
*
|
||||
* Copyright (C) 2010 Mike Anchor
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef _JSON_H_
|
||||
#define _JSON_H_
|
||||
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Simple function to check a string 's' has at least 'n' characters
|
||||
static inline bool simplejson_csnlen(const char *s, size_t n)
|
||||
{
|
||||
if (s == 0)
|
||||
return false;
|
||||
|
||||
const char *save = s;
|
||||
while (n-- > 0) {
|
||||
if (*(save++) == 0)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Custom types
|
||||
class JSONValue;
|
||||
typedef std::vector<JSONValue *> JSONArray;
|
||||
typedef std::map<std::string, JSONValue *> JSONObject;
|
||||
|
||||
#include "JSONValue.h"
|
||||
|
||||
class JSON
|
||||
{
|
||||
friend class JSONValue;
|
||||
|
||||
public:
|
||||
static JSONValue *Parse(const char *data);
|
||||
static std::string Stringify(const JSONValue *value);
|
||||
|
||||
protected:
|
||||
static bool SkipWhitespace(const char **data);
|
||||
static bool ExtractString(const char **data, std::string &str);
|
||||
static double ParseInt(const char **data);
|
||||
static double ParseDecimal(const char **data);
|
||||
|
||||
private:
|
||||
JSON();
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -1,897 +0,0 @@
|
||||
/*
|
||||
* File JSONValue.cpp part of the SimpleJSON Library - http://mjpa.in/json
|
||||
*
|
||||
* Copyright (C) 2010 Mike Anchor
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include <math.h>
|
||||
#include <sstream>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "JSONValue.h"
|
||||
|
||||
// Macros to free an array/object
|
||||
#define FREE_ARRAY(x) \
|
||||
{ \
|
||||
JSONArray::iterator iter; \
|
||||
for (iter = x.begin(); iter != x.end(); ++iter) { \
|
||||
delete *iter; \
|
||||
} \
|
||||
}
|
||||
#define FREE_OBJECT(x) \
|
||||
{ \
|
||||
JSONObject::iterator iter; \
|
||||
for (iter = x.begin(); iter != x.end(); ++iter) { \
|
||||
delete (*iter).second; \
|
||||
} \
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a JSON encoded value to a JSONValue object
|
||||
*
|
||||
* @access protected
|
||||
*
|
||||
* @param char** data Pointer to a char* that contains the data
|
||||
*
|
||||
* @return JSONValue* Returns a pointer to a JSONValue object on success, NULL on error
|
||||
*/
|
||||
JSONValue *JSONValue::Parse(const char **data)
|
||||
{
|
||||
// Is it a string?
|
||||
if (**data == '"') {
|
||||
std::string str;
|
||||
if (!JSON::ExtractString(&(++(*data)), str))
|
||||
return NULL;
|
||||
else
|
||||
return new JSONValue(str);
|
||||
}
|
||||
|
||||
// Is it a boolean?
|
||||
else if ((simplejson_csnlen(*data, 4) && strncasecmp(*data, "true", 4) == 0) ||
|
||||
(simplejson_csnlen(*data, 5) && strncasecmp(*data, "false", 5) == 0)) {
|
||||
bool value = strncasecmp(*data, "true", 4) == 0;
|
||||
(*data) += value ? 4 : 5;
|
||||
return new JSONValue(value);
|
||||
}
|
||||
|
||||
// Is it a null?
|
||||
else if (simplejson_csnlen(*data, 4) && strncasecmp(*data, "null", 4) == 0) {
|
||||
(*data) += 4;
|
||||
return new JSONValue();
|
||||
}
|
||||
|
||||
// Is it a number?
|
||||
else if (**data == '-' || (**data >= '0' && **data <= '9')) {
|
||||
// Negative?
|
||||
bool neg = **data == '-';
|
||||
if (neg)
|
||||
(*data)++;
|
||||
|
||||
double number = 0.0;
|
||||
|
||||
// Parse the whole part of the number - only if it wasn't 0
|
||||
if (**data == '0')
|
||||
(*data)++;
|
||||
else if (**data >= '1' && **data <= '9')
|
||||
number = JSON::ParseInt(data);
|
||||
else
|
||||
return NULL;
|
||||
|
||||
// Could be a decimal now...
|
||||
if (**data == '.') {
|
||||
(*data)++;
|
||||
|
||||
// Not get any digits?
|
||||
if (!(**data >= '0' && **data <= '9'))
|
||||
return NULL;
|
||||
|
||||
// Find the decimal and sort the decimal place out
|
||||
// Use ParseDecimal as ParseInt won't work with decimals less than 0.1
|
||||
// thanks to Javier Abadia for the report & fix
|
||||
double decimal = JSON::ParseDecimal(data);
|
||||
|
||||
// Save the number
|
||||
number += decimal;
|
||||
}
|
||||
|
||||
// Could be an exponent now...
|
||||
if (**data == 'E' || **data == 'e') {
|
||||
(*data)++;
|
||||
|
||||
// Check signage of expo
|
||||
bool neg_expo = false;
|
||||
if (**data == '-' || **data == '+') {
|
||||
neg_expo = **data == '-';
|
||||
(*data)++;
|
||||
}
|
||||
|
||||
// Not get any digits?
|
||||
if (!(**data >= '0' && **data <= '9'))
|
||||
return NULL;
|
||||
|
||||
// Sort the expo out
|
||||
double expo = JSON::ParseInt(data);
|
||||
for (double i = 0.0; i < expo; i++)
|
||||
number = neg_expo ? (number / 10.0) : (number * 10.0);
|
||||
}
|
||||
|
||||
// Was it neg?
|
||||
if (neg)
|
||||
number *= -1;
|
||||
|
||||
return new JSONValue(number);
|
||||
}
|
||||
|
||||
// An object?
|
||||
else if (**data == '{') {
|
||||
JSONObject object;
|
||||
|
||||
(*data)++;
|
||||
|
||||
while (**data != 0) {
|
||||
// Whitespace at the start?
|
||||
if (!JSON::SkipWhitespace(data)) {
|
||||
FREE_OBJECT(object);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Special case - empty object
|
||||
if (object.size() == 0 && **data == '}') {
|
||||
(*data)++;
|
||||
return new JSONValue(object);
|
||||
}
|
||||
|
||||
// We want a string now...
|
||||
std::string name;
|
||||
if (!JSON::ExtractString(&(++(*data)), name)) {
|
||||
FREE_OBJECT(object);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// More whitespace?
|
||||
if (!JSON::SkipWhitespace(data)) {
|
||||
FREE_OBJECT(object);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Need a : now
|
||||
if (*((*data)++) != ':') {
|
||||
FREE_OBJECT(object);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// More whitespace?
|
||||
if (!JSON::SkipWhitespace(data)) {
|
||||
FREE_OBJECT(object);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// The value is here
|
||||
JSONValue *value = Parse(data);
|
||||
if (value == NULL) {
|
||||
FREE_OBJECT(object);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Add the name:value
|
||||
if (object.find(name) != object.end())
|
||||
delete object[name];
|
||||
object[name] = value;
|
||||
|
||||
// More whitespace?
|
||||
if (!JSON::SkipWhitespace(data)) {
|
||||
FREE_OBJECT(object);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// End of object?
|
||||
if (**data == '}') {
|
||||
(*data)++;
|
||||
return new JSONValue(object);
|
||||
}
|
||||
|
||||
// Want a , now
|
||||
if (**data != ',') {
|
||||
FREE_OBJECT(object);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
(*data)++;
|
||||
}
|
||||
|
||||
// Only here if we ran out of data
|
||||
FREE_OBJECT(object);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// An array?
|
||||
else if (**data == '[') {
|
||||
JSONArray array;
|
||||
|
||||
(*data)++;
|
||||
|
||||
while (**data != 0) {
|
||||
// Whitespace at the start?
|
||||
if (!JSON::SkipWhitespace(data)) {
|
||||
FREE_ARRAY(array);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Special case - empty array
|
||||
if (array.size() == 0 && **data == ']') {
|
||||
(*data)++;
|
||||
return new JSONValue(array);
|
||||
}
|
||||
|
||||
// Get the value
|
||||
JSONValue *value = Parse(data);
|
||||
if (value == NULL) {
|
||||
FREE_ARRAY(array);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Add the value
|
||||
array.push_back(value);
|
||||
|
||||
// More whitespace?
|
||||
if (!JSON::SkipWhitespace(data)) {
|
||||
FREE_ARRAY(array);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// End of array?
|
||||
if (**data == ']') {
|
||||
(*data)++;
|
||||
return new JSONValue(array);
|
||||
}
|
||||
|
||||
// Want a , now
|
||||
if (**data != ',') {
|
||||
FREE_ARRAY(array);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
(*data)++;
|
||||
}
|
||||
|
||||
// Only here if we ran out of data
|
||||
FREE_ARRAY(array);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Ran out of possibilities, it's bad!
|
||||
else {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic constructor for creating a JSON Value of type NULL
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
JSONValue::JSONValue(/*NULL*/)
|
||||
{
|
||||
type = JSONType_Null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic constructor for creating a JSON Value of type String
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param char* m_char_value The string to use as the value
|
||||
*/
|
||||
JSONValue::JSONValue(const char *m_char_value)
|
||||
{
|
||||
type = JSONType_String;
|
||||
string_value = new std::string(std::string(m_char_value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic constructor for creating a JSON Value of type String
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param std::string m_string_value The string to use as the value
|
||||
*/
|
||||
JSONValue::JSONValue(const std::string &m_string_value)
|
||||
{
|
||||
type = JSONType_String;
|
||||
string_value = new std::string(m_string_value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic constructor for creating a JSON Value of type Bool
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param bool m_bool_value The bool to use as the value
|
||||
*/
|
||||
JSONValue::JSONValue(bool m_bool_value)
|
||||
{
|
||||
type = JSONType_Bool;
|
||||
bool_value = m_bool_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic constructor for creating a JSON Value of type Number
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param double m_number_value The number to use as the value
|
||||
*/
|
||||
JSONValue::JSONValue(double m_number_value)
|
||||
{
|
||||
type = JSONType_Number;
|
||||
number_value = m_number_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic constructor for creating a JSON Value of type Number
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param int m_integer_value The number to use as the value
|
||||
*/
|
||||
JSONValue::JSONValue(int m_integer_value)
|
||||
{
|
||||
type = JSONType_Number;
|
||||
number_value = (double)m_integer_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic constructor for creating a JSON Value of type Number
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param unsigned int m_integer_value The number to use as the value
|
||||
*/
|
||||
JSONValue::JSONValue(unsigned int m_integer_value)
|
||||
{
|
||||
type = JSONType_Number;
|
||||
number_value = (double)m_integer_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic constructor for creating a JSON Value of type Array
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param JSONArray m_array_value The JSONArray to use as the value
|
||||
*/
|
||||
JSONValue::JSONValue(const JSONArray &m_array_value)
|
||||
{
|
||||
type = JSONType_Array;
|
||||
array_value = new JSONArray(m_array_value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic constructor for creating a JSON Value of type Object
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param JSONObject m_object_value The JSONObject to use as the value
|
||||
*/
|
||||
JSONValue::JSONValue(const JSONObject &m_object_value)
|
||||
{
|
||||
type = JSONType_Object;
|
||||
object_value = new JSONObject(m_object_value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy constructor to perform a deep copy of array / object values
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param JSONValue m_source The source JSONValue that is being copied
|
||||
*/
|
||||
JSONValue::JSONValue(const JSONValue &m_source)
|
||||
{
|
||||
type = m_source.type;
|
||||
|
||||
switch (type) {
|
||||
case JSONType_String:
|
||||
string_value = new std::string(*m_source.string_value);
|
||||
break;
|
||||
|
||||
case JSONType_Bool:
|
||||
bool_value = m_source.bool_value;
|
||||
break;
|
||||
|
||||
case JSONType_Number:
|
||||
number_value = m_source.number_value;
|
||||
break;
|
||||
|
||||
case JSONType_Array: {
|
||||
JSONArray source_array = *m_source.array_value;
|
||||
JSONArray::iterator iter;
|
||||
array_value = new JSONArray();
|
||||
for (iter = source_array.begin(); iter != source_array.end(); ++iter)
|
||||
array_value->push_back(new JSONValue(**iter));
|
||||
break;
|
||||
}
|
||||
|
||||
case JSONType_Object: {
|
||||
JSONObject source_object = *m_source.object_value;
|
||||
object_value = new JSONObject();
|
||||
JSONObject::iterator iter;
|
||||
for (iter = source_object.begin(); iter != source_object.end(); ++iter) {
|
||||
std::string name = (*iter).first;
|
||||
(*object_value)[name] = new JSONValue(*((*iter).second));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case JSONType_Null:
|
||||
// Nothing to do.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The destructor for the JSON Value object
|
||||
* Handles deleting the objects in the array or the object value
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
JSONValue::~JSONValue()
|
||||
{
|
||||
if (type == JSONType_Array) {
|
||||
JSONArray::iterator iter;
|
||||
for (iter = array_value->begin(); iter != array_value->end(); ++iter)
|
||||
delete *iter;
|
||||
delete array_value;
|
||||
} else if (type == JSONType_Object) {
|
||||
JSONObject::iterator iter;
|
||||
for (iter = object_value->begin(); iter != object_value->end(); ++iter) {
|
||||
delete (*iter).second;
|
||||
}
|
||||
delete object_value;
|
||||
} else if (type == JSONType_String) {
|
||||
delete string_value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the value is a NULL
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return bool Returns true if it is a NULL value, false otherwise
|
||||
*/
|
||||
bool JSONValue::IsNull() const
|
||||
{
|
||||
return type == JSONType_Null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the value is a String
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return bool Returns true if it is a String value, false otherwise
|
||||
*/
|
||||
bool JSONValue::IsString() const
|
||||
{
|
||||
return type == JSONType_String;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the value is a Bool
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return bool Returns true if it is a Bool value, false otherwise
|
||||
*/
|
||||
bool JSONValue::IsBool() const
|
||||
{
|
||||
return type == JSONType_Bool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the value is a Number
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return bool Returns true if it is a Number value, false otherwise
|
||||
*/
|
||||
bool JSONValue::IsNumber() const
|
||||
{
|
||||
return type == JSONType_Number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the value is an Array
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return bool Returns true if it is an Array value, false otherwise
|
||||
*/
|
||||
bool JSONValue::IsArray() const
|
||||
{
|
||||
return type == JSONType_Array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the value is an Object
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return bool Returns true if it is an Object value, false otherwise
|
||||
*/
|
||||
bool JSONValue::IsObject() const
|
||||
{
|
||||
return type == JSONType_Object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the String value of this JSONValue
|
||||
* Use IsString() before using this method.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return std::string Returns the string value
|
||||
*/
|
||||
const std::string &JSONValue::AsString() const
|
||||
{
|
||||
return (*string_value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the Bool value of this JSONValue
|
||||
* Use IsBool() before using this method.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return bool Returns the bool value
|
||||
*/
|
||||
bool JSONValue::AsBool() const
|
||||
{
|
||||
return bool_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the Number value of this JSONValue
|
||||
* Use IsNumber() before using this method.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return double Returns the number value
|
||||
*/
|
||||
double JSONValue::AsNumber() const
|
||||
{
|
||||
return number_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the Array value of this JSONValue
|
||||
* Use IsArray() before using this method.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return JSONArray Returns the array value
|
||||
*/
|
||||
const JSONArray &JSONValue::AsArray() const
|
||||
{
|
||||
return (*array_value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the Object value of this JSONValue
|
||||
* Use IsObject() before using this method.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return JSONObject Returns the object value
|
||||
*/
|
||||
const JSONObject &JSONValue::AsObject() const
|
||||
{
|
||||
return (*object_value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the number of children of this JSONValue.
|
||||
* This number will be 0 or the actual number of children
|
||||
* if IsArray() or IsObject().
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return The number of children.
|
||||
*/
|
||||
std::size_t JSONValue::CountChildren() const
|
||||
{
|
||||
switch (type) {
|
||||
case JSONType_Array:
|
||||
return array_value->size();
|
||||
case JSONType_Object:
|
||||
return object_value->size();
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this JSONValue has a child at the given index.
|
||||
* Use IsArray() before using this method.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return bool Returns true if the array has a value at the given index.
|
||||
*/
|
||||
bool JSONValue::HasChild(std::size_t index) const
|
||||
{
|
||||
if (type == JSONType_Array) {
|
||||
return index < array_value->size();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the child of this JSONValue at the given index.
|
||||
* Use IsArray() before using this method.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return JSONValue* Returns JSONValue at the given index or NULL
|
||||
* if it doesn't exist.
|
||||
*/
|
||||
JSONValue *JSONValue::Child(std::size_t index)
|
||||
{
|
||||
if (index < array_value->size()) {
|
||||
return (*array_value)[index];
|
||||
} else {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this JSONValue has a child at the given key.
|
||||
* Use IsObject() before using this method.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return bool Returns true if the object has a value at the given key.
|
||||
*/
|
||||
bool JSONValue::HasChild(const char *name) const
|
||||
{
|
||||
if (type == JSONType_Object) {
|
||||
return object_value->find(name) != object_value->end();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the child of this JSONValue at the given key.
|
||||
* Use IsObject() before using this method.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return JSONValue* Returns JSONValue for the given key in the object
|
||||
* or NULL if it doesn't exist.
|
||||
*/
|
||||
JSONValue *JSONValue::Child(const char *name)
|
||||
{
|
||||
JSONObject::const_iterator it = object_value->find(name);
|
||||
if (it != object_value->end()) {
|
||||
return it->second;
|
||||
} else {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the keys of the JSON Object or an empty vector
|
||||
* if this value is not an object.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return std::vector<std::string> A vector containing the keys.
|
||||
*/
|
||||
std::vector<std::string> JSONValue::ObjectKeys() const
|
||||
{
|
||||
std::vector<std::string> keys;
|
||||
|
||||
if (type == JSONType_Object) {
|
||||
JSONObject::const_iterator iter = object_value->begin();
|
||||
while (iter != object_value->end()) {
|
||||
keys.push_back(iter->first);
|
||||
|
||||
++iter;
|
||||
}
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a JSON encoded string for the value with all necessary characters escaped
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param bool prettyprint Enable prettyprint
|
||||
*
|
||||
* @return std::string Returns the JSON string
|
||||
*/
|
||||
std::string JSONValue::Stringify(bool const prettyprint) const
|
||||
{
|
||||
size_t const indentDepth = prettyprint ? 1 : 0;
|
||||
return StringifyImpl(indentDepth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a JSON encoded string for the value with all necessary characters escaped
|
||||
*
|
||||
* @access private
|
||||
*
|
||||
* @param size_t indentDepth The prettyprint indentation depth (0 : no prettyprint)
|
||||
*
|
||||
* @return std::string Returns the JSON string
|
||||
*/
|
||||
std::string JSONValue::StringifyImpl(size_t const indentDepth) const
|
||||
{
|
||||
std::string ret_string;
|
||||
size_t const indentDepth1 = indentDepth ? indentDepth + 1 : 0;
|
||||
std::string const indentStr = Indent(indentDepth);
|
||||
std::string const indentStr1 = Indent(indentDepth1);
|
||||
|
||||
switch (type) {
|
||||
case JSONType_Null:
|
||||
ret_string = "null";
|
||||
break;
|
||||
|
||||
case JSONType_String:
|
||||
ret_string = StringifyString(*string_value);
|
||||
break;
|
||||
|
||||
case JSONType_Bool:
|
||||
ret_string = bool_value ? "true" : "false";
|
||||
break;
|
||||
|
||||
case JSONType_Number: {
|
||||
if (isinf(number_value) || isnan(number_value))
|
||||
ret_string = "null";
|
||||
else {
|
||||
std::stringstream ss;
|
||||
ss.precision(15);
|
||||
ss << number_value;
|
||||
ret_string = ss.str();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case JSONType_Array: {
|
||||
ret_string = indentDepth ? "[\n" + indentStr1 : "[";
|
||||
JSONArray::const_iterator iter = array_value->begin();
|
||||
while (iter != array_value->end()) {
|
||||
ret_string += (*iter)->StringifyImpl(indentDepth1);
|
||||
|
||||
// Not at the end - add a separator
|
||||
if (++iter != array_value->end())
|
||||
ret_string += ",";
|
||||
}
|
||||
ret_string += indentDepth ? "\n" + indentStr + "]" : "]";
|
||||
break;
|
||||
}
|
||||
|
||||
case JSONType_Object: {
|
||||
ret_string = indentDepth ? "{\n" + indentStr1 : "{";
|
||||
JSONObject::const_iterator iter = object_value->begin();
|
||||
while (iter != object_value->end()) {
|
||||
ret_string += StringifyString((*iter).first);
|
||||
ret_string += ":";
|
||||
ret_string += (*iter).second->StringifyImpl(indentDepth1);
|
||||
|
||||
// Not at the end - add a separator
|
||||
if (++iter != object_value->end())
|
||||
ret_string += ",";
|
||||
}
|
||||
ret_string += indentDepth ? "\n" + indentStr + "}" : "}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ret_string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a JSON encoded string with all required fields escaped
|
||||
* Works from http://www.ecma-internationl.org/publications/files/ECMA-ST/ECMA-262.pdf
|
||||
* Section 15.12.3.
|
||||
*
|
||||
* @access private
|
||||
*
|
||||
* @param std::string str The string that needs to have the characters escaped
|
||||
*
|
||||
* @return std::string Returns the JSON string
|
||||
*/
|
||||
std::string JSONValue::StringifyString(const std::string &str)
|
||||
{
|
||||
std::string str_out = "\"";
|
||||
|
||||
std::string::const_iterator iter = str.begin();
|
||||
while (iter != str.end()) {
|
||||
char chr = *iter;
|
||||
|
||||
if (chr == '"' || chr == '\\' || chr == '/') {
|
||||
str_out += '\\';
|
||||
str_out += chr;
|
||||
} else if (chr == '\b') {
|
||||
str_out += "\\b";
|
||||
} else if (chr == '\f') {
|
||||
str_out += "\\f";
|
||||
} else if (chr == '\n') {
|
||||
str_out += "\\n";
|
||||
} else if (chr == '\r') {
|
||||
str_out += "\\r";
|
||||
} else if (chr == '\t') {
|
||||
str_out += "\\t";
|
||||
} else if (chr < 0x20 || chr == 0x7F) {
|
||||
char buf[7];
|
||||
snprintf(buf, sizeof(buf), "\\u%04x", chr);
|
||||
str_out += buf;
|
||||
} else if (chr < 0x80) {
|
||||
str_out += chr;
|
||||
} else {
|
||||
str_out += chr;
|
||||
size_t remain = str.end() - iter - 1;
|
||||
if ((chr & 0xE0) == 0xC0 && remain >= 1) {
|
||||
++iter;
|
||||
str_out += *iter;
|
||||
} else if ((chr & 0xF0) == 0xE0 && remain >= 2) {
|
||||
str_out += *(++iter);
|
||||
str_out += *(++iter);
|
||||
} else if ((chr & 0xF8) == 0xF0 && remain >= 3) {
|
||||
str_out += *(++iter);
|
||||
str_out += *(++iter);
|
||||
str_out += *(++iter);
|
||||
}
|
||||
}
|
||||
|
||||
++iter;
|
||||
}
|
||||
|
||||
str_out += "\"";
|
||||
return str_out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the indentation string for the depth given
|
||||
*
|
||||
* @access private
|
||||
*
|
||||
* @param size_t indent The prettyprint indentation depth (0 : no indentation)
|
||||
*
|
||||
* @return std::string Returns the string
|
||||
*/
|
||||
std::string JSONValue::Indent(size_t depth)
|
||||
{
|
||||
const size_t indent_step = 2;
|
||||
depth ? --depth : 0;
|
||||
std::string indentStr(depth * indent_step, ' ');
|
||||
return indentStr;
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
/*
|
||||
* File JSONValue.h part of the SimpleJSON Library - http://mjpa.in/json
|
||||
*
|
||||
* Copyright (C) 2010 Mike Anchor
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef _JSONVALUE_H_
|
||||
#define _JSONVALUE_H_
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "JSON.h"
|
||||
|
||||
class JSON;
|
||||
|
||||
enum JSONType { JSONType_Null, JSONType_String, JSONType_Bool, JSONType_Number, JSONType_Array, JSONType_Object };
|
||||
|
||||
class JSONValue
|
||||
{
|
||||
friend class JSON;
|
||||
|
||||
public:
|
||||
JSONValue(/*NULL*/);
|
||||
explicit JSONValue(const char *m_char_value);
|
||||
explicit JSONValue(const std::string &m_string_value);
|
||||
explicit JSONValue(bool m_bool_value);
|
||||
explicit JSONValue(double m_number_value);
|
||||
explicit JSONValue(int m_integer_value);
|
||||
explicit JSONValue(unsigned int m_integer_value);
|
||||
explicit JSONValue(const JSONArray &m_array_value);
|
||||
explicit JSONValue(const JSONObject &m_object_value);
|
||||
explicit JSONValue(const JSONValue &m_source);
|
||||
~JSONValue();
|
||||
|
||||
bool IsNull() const;
|
||||
bool IsString() const;
|
||||
bool IsBool() const;
|
||||
bool IsNumber() const;
|
||||
bool IsArray() const;
|
||||
bool IsObject() const;
|
||||
|
||||
const std::string &AsString() const;
|
||||
bool AsBool() const;
|
||||
double AsNumber() const;
|
||||
const JSONArray &AsArray() const;
|
||||
const JSONObject &AsObject() const;
|
||||
|
||||
std::size_t CountChildren() const;
|
||||
bool HasChild(std::size_t index) const;
|
||||
JSONValue *Child(std::size_t index);
|
||||
bool HasChild(const char *name) const;
|
||||
JSONValue *Child(const char *name);
|
||||
std::vector<std::string> ObjectKeys() const;
|
||||
|
||||
std::string Stringify(bool const prettyprint = false) const;
|
||||
|
||||
protected:
|
||||
static JSONValue *Parse(const char **data);
|
||||
|
||||
private:
|
||||
static std::string StringifyString(const std::string &str);
|
||||
std::string StringifyImpl(size_t const indentDepth) const;
|
||||
static std::string Indent(size_t depth);
|
||||
|
||||
JSONType type;
|
||||
|
||||
union {
|
||||
bool bool_value;
|
||||
double number_value;
|
||||
std::string *string_value;
|
||||
JSONArray *array_value;
|
||||
JSONObject *object_value;
|
||||
};
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -1,11 +1,12 @@
|
||||
#ifndef NRF52_USE_JSON
|
||||
#if ARCH_PORTDUINO
|
||||
#include "MeshPacketSerializer.h"
|
||||
#include "JSON.h"
|
||||
#include "NodeDB.h"
|
||||
#include "mesh/generated/meshtastic/mqtt.pb.h"
|
||||
#include "mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "modules/RoutingModule.h"
|
||||
#include <DebugConfiguration.h>
|
||||
#include <json/json.h>
|
||||
#include <memory>
|
||||
#include <mesh-pb-constants.h>
|
||||
#if defined(ARCH_ESP32)
|
||||
#include "../mesh/generated/meshtastic/paxcount.pb.h"
|
||||
@@ -15,41 +16,49 @@
|
||||
|
||||
static const char *errStr = "Error decoding proto for %s message!";
|
||||
|
||||
static std::string writeCompact(const Json::Value &v)
|
||||
{
|
||||
Json::StreamWriterBuilder b;
|
||||
b["indentation"] = "";
|
||||
b["emitUTF8"] = true;
|
||||
return Json::writeString(b, v);
|
||||
}
|
||||
|
||||
static bool tryParseJson(const char *s, Json::Value &out)
|
||||
{
|
||||
Json::CharReaderBuilder b;
|
||||
std::unique_ptr<Json::CharReader> reader(b.newCharReader());
|
||||
std::string errs;
|
||||
const char *end = s + strlen(s);
|
||||
return reader->parse(s, end, &out, &errs);
|
||||
}
|
||||
|
||||
std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, bool shouldLog)
|
||||
{
|
||||
// the created jsonObj is immutable after creation, so
|
||||
// we need to do the heavy lifting before assembling it.
|
||||
std::string msgType;
|
||||
JSONObject jsonObj;
|
||||
Json::Value jsonObj(Json::objectValue);
|
||||
|
||||
if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
|
||||
JSONObject msgPayload;
|
||||
Json::Value msgPayload(Json::objectValue);
|
||||
switch (mp->decoded.portnum) {
|
||||
case meshtastic_PortNum_TEXT_MESSAGE_APP: {
|
||||
msgType = "text";
|
||||
// convert bytes to string
|
||||
if (shouldLog)
|
||||
LOG_DEBUG("got text message of size %u", mp->decoded.payload.size);
|
||||
|
||||
char payloadStr[(mp->decoded.payload.size) + 1];
|
||||
memcpy(payloadStr, mp->decoded.payload.bytes, mp->decoded.payload.size);
|
||||
payloadStr[mp->decoded.payload.size] = 0; // null terminated string
|
||||
// check if this is a JSON payload
|
||||
JSONValue *json_value = JSON::Parse(payloadStr);
|
||||
if (json_value != NULL) {
|
||||
payloadStr[mp->decoded.payload.size] = 0;
|
||||
Json::Value parsed;
|
||||
if (tryParseJson(payloadStr, parsed)) {
|
||||
if (shouldLog)
|
||||
LOG_INFO("text message payload is of type json");
|
||||
|
||||
// if it is, then we can just use the json object
|
||||
jsonObj["payload"] = json_value;
|
||||
jsonObj["payload"] = parsed;
|
||||
} else {
|
||||
// if it isn't, then we need to create a json object
|
||||
// with the string as the value
|
||||
if (shouldLog)
|
||||
LOG_INFO("text message payload is of type plaintext");
|
||||
|
||||
msgPayload["text"] = new JSONValue(payloadStr);
|
||||
jsonObj["payload"] = new JSONValue(msgPayload);
|
||||
msgPayload["text"] = payloadStr;
|
||||
jsonObj["payload"] = msgPayload;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -61,133 +70,129 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
|
||||
if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Telemetry_msg, &scratch)) {
|
||||
decoded = &scratch;
|
||||
if (decoded->which_variant == meshtastic_Telemetry_device_metrics_tag) {
|
||||
// If battery is present, encode the battery level value
|
||||
// TODO - Add a condition to send a code for a non-present value
|
||||
if (decoded->variant.device_metrics.has_battery_level) {
|
||||
msgPayload["battery_level"] = new JSONValue((int)decoded->variant.device_metrics.battery_level);
|
||||
msgPayload["battery_level"] = (int)decoded->variant.device_metrics.battery_level;
|
||||
}
|
||||
msgPayload["voltage"] = new JSONValue(decoded->variant.device_metrics.voltage);
|
||||
msgPayload["channel_utilization"] = new JSONValue(decoded->variant.device_metrics.channel_utilization);
|
||||
msgPayload["air_util_tx"] = new JSONValue(decoded->variant.device_metrics.air_util_tx);
|
||||
msgPayload["uptime_seconds"] = new JSONValue((unsigned int)decoded->variant.device_metrics.uptime_seconds);
|
||||
msgPayload["voltage"] = decoded->variant.device_metrics.voltage;
|
||||
msgPayload["channel_utilization"] = decoded->variant.device_metrics.channel_utilization;
|
||||
msgPayload["air_util_tx"] = decoded->variant.device_metrics.air_util_tx;
|
||||
msgPayload["uptime_seconds"] = (Json::UInt)decoded->variant.device_metrics.uptime_seconds;
|
||||
} else if (decoded->which_variant == meshtastic_Telemetry_environment_metrics_tag) {
|
||||
// Avoid sending 0s for sensors that could be 0
|
||||
if (decoded->variant.environment_metrics.has_temperature) {
|
||||
msgPayload["temperature"] = new JSONValue(decoded->variant.environment_metrics.temperature);
|
||||
msgPayload["temperature"] = decoded->variant.environment_metrics.temperature;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_relative_humidity) {
|
||||
msgPayload["relative_humidity"] = new JSONValue(decoded->variant.environment_metrics.relative_humidity);
|
||||
msgPayload["relative_humidity"] = decoded->variant.environment_metrics.relative_humidity;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_barometric_pressure) {
|
||||
msgPayload["barometric_pressure"] =
|
||||
new JSONValue(decoded->variant.environment_metrics.barometric_pressure);
|
||||
msgPayload["barometric_pressure"] = decoded->variant.environment_metrics.barometric_pressure;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_gas_resistance) {
|
||||
msgPayload["gas_resistance"] = new JSONValue(decoded->variant.environment_metrics.gas_resistance);
|
||||
msgPayload["gas_resistance"] = decoded->variant.environment_metrics.gas_resistance;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_voltage) {
|
||||
msgPayload["voltage"] = new JSONValue(decoded->variant.environment_metrics.voltage);
|
||||
msgPayload["voltage"] = decoded->variant.environment_metrics.voltage;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_current) {
|
||||
msgPayload["current"] = new JSONValue(decoded->variant.environment_metrics.current);
|
||||
msgPayload["current"] = decoded->variant.environment_metrics.current;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_lux) {
|
||||
msgPayload["lux"] = new JSONValue(decoded->variant.environment_metrics.lux);
|
||||
msgPayload["lux"] = decoded->variant.environment_metrics.lux;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_white_lux) {
|
||||
msgPayload["white_lux"] = new JSONValue(decoded->variant.environment_metrics.white_lux);
|
||||
msgPayload["white_lux"] = decoded->variant.environment_metrics.white_lux;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_iaq) {
|
||||
msgPayload["iaq"] = new JSONValue((uint)decoded->variant.environment_metrics.iaq);
|
||||
msgPayload["iaq"] = (Json::UInt)decoded->variant.environment_metrics.iaq;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_distance) {
|
||||
msgPayload["distance"] = new JSONValue(decoded->variant.environment_metrics.distance);
|
||||
msgPayload["distance"] = decoded->variant.environment_metrics.distance;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_wind_speed) {
|
||||
msgPayload["wind_speed"] = new JSONValue(decoded->variant.environment_metrics.wind_speed);
|
||||
msgPayload["wind_speed"] = decoded->variant.environment_metrics.wind_speed;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_wind_direction) {
|
||||
msgPayload["wind_direction"] = new JSONValue((uint)decoded->variant.environment_metrics.wind_direction);
|
||||
msgPayload["wind_direction"] = (Json::UInt)decoded->variant.environment_metrics.wind_direction;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_wind_gust) {
|
||||
msgPayload["wind_gust"] = new JSONValue(decoded->variant.environment_metrics.wind_gust);
|
||||
msgPayload["wind_gust"] = decoded->variant.environment_metrics.wind_gust;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_wind_lull) {
|
||||
msgPayload["wind_lull"] = new JSONValue(decoded->variant.environment_metrics.wind_lull);
|
||||
msgPayload["wind_lull"] = decoded->variant.environment_metrics.wind_lull;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_radiation) {
|
||||
msgPayload["radiation"] = new JSONValue(decoded->variant.environment_metrics.radiation);
|
||||
msgPayload["radiation"] = decoded->variant.environment_metrics.radiation;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_ir_lux) {
|
||||
msgPayload["ir_lux"] = new JSONValue(decoded->variant.environment_metrics.ir_lux);
|
||||
msgPayload["ir_lux"] = decoded->variant.environment_metrics.ir_lux;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_uv_lux) {
|
||||
msgPayload["uv_lux"] = new JSONValue(decoded->variant.environment_metrics.uv_lux);
|
||||
msgPayload["uv_lux"] = decoded->variant.environment_metrics.uv_lux;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_weight) {
|
||||
msgPayload["weight"] = new JSONValue(decoded->variant.environment_metrics.weight);
|
||||
msgPayload["weight"] = decoded->variant.environment_metrics.weight;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_rainfall_1h) {
|
||||
msgPayload["rainfall_1h"] = new JSONValue(decoded->variant.environment_metrics.rainfall_1h);
|
||||
msgPayload["rainfall_1h"] = decoded->variant.environment_metrics.rainfall_1h;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_rainfall_24h) {
|
||||
msgPayload["rainfall_24h"] = new JSONValue(decoded->variant.environment_metrics.rainfall_24h);
|
||||
msgPayload["rainfall_24h"] = decoded->variant.environment_metrics.rainfall_24h;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_soil_moisture) {
|
||||
msgPayload["soil_moisture"] = new JSONValue((uint)decoded->variant.environment_metrics.soil_moisture);
|
||||
msgPayload["soil_moisture"] = (Json::UInt)decoded->variant.environment_metrics.soil_moisture;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_soil_temperature) {
|
||||
msgPayload["soil_temperature"] = new JSONValue(decoded->variant.environment_metrics.soil_temperature);
|
||||
msgPayload["soil_temperature"] = decoded->variant.environment_metrics.soil_temperature;
|
||||
}
|
||||
} else if (decoded->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) {
|
||||
if (decoded->variant.air_quality_metrics.has_pm10_standard) {
|
||||
msgPayload["pm10"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_standard);
|
||||
msgPayload["pm10"] = (Json::UInt)decoded->variant.air_quality_metrics.pm10_standard;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_pm25_standard) {
|
||||
msgPayload["pm25"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_standard);
|
||||
msgPayload["pm25"] = (Json::UInt)decoded->variant.air_quality_metrics.pm25_standard;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_pm100_standard) {
|
||||
msgPayload["pm100"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_standard);
|
||||
msgPayload["pm100"] = (Json::UInt)decoded->variant.air_quality_metrics.pm100_standard;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_co2) {
|
||||
msgPayload["co2"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.co2);
|
||||
msgPayload["co2"] = (Json::UInt)decoded->variant.air_quality_metrics.co2;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_co2_temperature) {
|
||||
msgPayload["co2_temperature"] = new JSONValue(decoded->variant.air_quality_metrics.co2_temperature);
|
||||
msgPayload["co2_temperature"] = decoded->variant.air_quality_metrics.co2_temperature;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_co2_humidity) {
|
||||
msgPayload["co2_humidity"] = new JSONValue(decoded->variant.air_quality_metrics.co2_humidity);
|
||||
msgPayload["co2_humidity"] = decoded->variant.air_quality_metrics.co2_humidity;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_form_formaldehyde) {
|
||||
msgPayload["form_formaldehyde"] = new JSONValue(decoded->variant.air_quality_metrics.form_formaldehyde);
|
||||
msgPayload["form_formaldehyde"] = decoded->variant.air_quality_metrics.form_formaldehyde;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_form_temperature) {
|
||||
msgPayload["form_temperature"] = new JSONValue(decoded->variant.air_quality_metrics.form_temperature);
|
||||
msgPayload["form_temperature"] = decoded->variant.air_quality_metrics.form_temperature;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_form_humidity) {
|
||||
msgPayload["form_humidity"] = new JSONValue(decoded->variant.air_quality_metrics.form_humidity);
|
||||
msgPayload["form_humidity"] = decoded->variant.air_quality_metrics.form_humidity;
|
||||
}
|
||||
} else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) {
|
||||
if (decoded->variant.power_metrics.has_ch1_voltage) {
|
||||
msgPayload["voltage_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_voltage);
|
||||
msgPayload["voltage_ch1"] = decoded->variant.power_metrics.ch1_voltage;
|
||||
}
|
||||
if (decoded->variant.power_metrics.has_ch1_current) {
|
||||
msgPayload["current_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_current);
|
||||
msgPayload["current_ch1"] = decoded->variant.power_metrics.ch1_current;
|
||||
}
|
||||
if (decoded->variant.power_metrics.has_ch2_voltage) {
|
||||
msgPayload["voltage_ch2"] = new JSONValue(decoded->variant.power_metrics.ch2_voltage);
|
||||
msgPayload["voltage_ch2"] = decoded->variant.power_metrics.ch2_voltage;
|
||||
}
|
||||
if (decoded->variant.power_metrics.has_ch2_current) {
|
||||
msgPayload["current_ch2"] = new JSONValue(decoded->variant.power_metrics.ch2_current);
|
||||
msgPayload["current_ch2"] = decoded->variant.power_metrics.ch2_current;
|
||||
}
|
||||
if (decoded->variant.power_metrics.has_ch3_voltage) {
|
||||
msgPayload["voltage_ch3"] = new JSONValue(decoded->variant.power_metrics.ch3_voltage);
|
||||
msgPayload["voltage_ch3"] = decoded->variant.power_metrics.ch3_voltage;
|
||||
}
|
||||
if (decoded->variant.power_metrics.has_ch3_current) {
|
||||
msgPayload["current_ch3"] = new JSONValue(decoded->variant.power_metrics.ch3_current);
|
||||
msgPayload["current_ch3"] = decoded->variant.power_metrics.ch3_current;
|
||||
}
|
||||
}
|
||||
jsonObj["payload"] = new JSONValue(msgPayload);
|
||||
jsonObj["payload"] = msgPayload;
|
||||
} else if (shouldLog) {
|
||||
LOG_ERROR(errStr, msgType.c_str());
|
||||
}
|
||||
@@ -200,12 +205,12 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
|
||||
memset(&scratch, 0, sizeof(scratch));
|
||||
if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_User_msg, &scratch)) {
|
||||
decoded = &scratch;
|
||||
msgPayload["id"] = new JSONValue(decoded->id);
|
||||
msgPayload["longname"] = new JSONValue(decoded->long_name);
|
||||
msgPayload["shortname"] = new JSONValue(decoded->short_name);
|
||||
msgPayload["hardware"] = new JSONValue(decoded->hw_model);
|
||||
msgPayload["role"] = new JSONValue((int)decoded->role);
|
||||
jsonObj["payload"] = new JSONValue(msgPayload);
|
||||
msgPayload["id"] = decoded->id;
|
||||
msgPayload["longname"] = decoded->long_name;
|
||||
msgPayload["shortname"] = decoded->short_name;
|
||||
msgPayload["hardware"] = (int)decoded->hw_model;
|
||||
msgPayload["role"] = (int)decoded->role;
|
||||
jsonObj["payload"] = msgPayload;
|
||||
} else if (shouldLog) {
|
||||
LOG_ERROR(errStr, msgType.c_str());
|
||||
}
|
||||
@@ -219,38 +224,38 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
|
||||
if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Position_msg, &scratch)) {
|
||||
decoded = &scratch;
|
||||
if ((int)decoded->time) {
|
||||
msgPayload["time"] = new JSONValue((unsigned int)decoded->time);
|
||||
msgPayload["time"] = (Json::UInt)decoded->time;
|
||||
}
|
||||
if ((int)decoded->timestamp) {
|
||||
msgPayload["timestamp"] = new JSONValue((unsigned int)decoded->timestamp);
|
||||
msgPayload["timestamp"] = (Json::UInt)decoded->timestamp;
|
||||
}
|
||||
msgPayload["latitude_i"] = new JSONValue((int)decoded->latitude_i);
|
||||
msgPayload["longitude_i"] = new JSONValue((int)decoded->longitude_i);
|
||||
msgPayload["latitude_i"] = (int)decoded->latitude_i;
|
||||
msgPayload["longitude_i"] = (int)decoded->longitude_i;
|
||||
if ((int)decoded->altitude) {
|
||||
msgPayload["altitude"] = new JSONValue((int)decoded->altitude);
|
||||
msgPayload["altitude"] = (int)decoded->altitude;
|
||||
}
|
||||
if ((int)decoded->ground_speed) {
|
||||
msgPayload["ground_speed"] = new JSONValue((unsigned int)decoded->ground_speed);
|
||||
msgPayload["ground_speed"] = (Json::UInt)decoded->ground_speed;
|
||||
}
|
||||
if (int(decoded->ground_track)) {
|
||||
msgPayload["ground_track"] = new JSONValue((unsigned int)decoded->ground_track);
|
||||
msgPayload["ground_track"] = (Json::UInt)decoded->ground_track;
|
||||
}
|
||||
if (int(decoded->sats_in_view)) {
|
||||
msgPayload["sats_in_view"] = new JSONValue((unsigned int)decoded->sats_in_view);
|
||||
msgPayload["sats_in_view"] = (Json::UInt)decoded->sats_in_view;
|
||||
}
|
||||
if ((int)decoded->PDOP) {
|
||||
msgPayload["PDOP"] = new JSONValue((int)decoded->PDOP);
|
||||
msgPayload["PDOP"] = (int)decoded->PDOP;
|
||||
}
|
||||
if ((int)decoded->HDOP) {
|
||||
msgPayload["HDOP"] = new JSONValue((int)decoded->HDOP);
|
||||
msgPayload["HDOP"] = (int)decoded->HDOP;
|
||||
}
|
||||
if ((int)decoded->VDOP) {
|
||||
msgPayload["VDOP"] = new JSONValue((int)decoded->VDOP);
|
||||
msgPayload["VDOP"] = (int)decoded->VDOP;
|
||||
}
|
||||
if ((int)decoded->precision_bits) {
|
||||
msgPayload["precision_bits"] = new JSONValue((int)decoded->precision_bits);
|
||||
msgPayload["precision_bits"] = (int)decoded->precision_bits;
|
||||
}
|
||||
jsonObj["payload"] = new JSONValue(msgPayload);
|
||||
jsonObj["payload"] = msgPayload;
|
||||
} else if (shouldLog) {
|
||||
LOG_ERROR(errStr, msgType.c_str());
|
||||
}
|
||||
@@ -263,14 +268,14 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
|
||||
memset(&scratch, 0, sizeof(scratch));
|
||||
if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Waypoint_msg, &scratch)) {
|
||||
decoded = &scratch;
|
||||
msgPayload["id"] = new JSONValue((unsigned int)decoded->id);
|
||||
msgPayload["name"] = new JSONValue(decoded->name);
|
||||
msgPayload["description"] = new JSONValue(decoded->description);
|
||||
msgPayload["expire"] = new JSONValue((unsigned int)decoded->expire);
|
||||
msgPayload["locked_to"] = new JSONValue((unsigned int)decoded->locked_to);
|
||||
msgPayload["latitude_i"] = new JSONValue((int)decoded->latitude_i);
|
||||
msgPayload["longitude_i"] = new JSONValue((int)decoded->longitude_i);
|
||||
jsonObj["payload"] = new JSONValue(msgPayload);
|
||||
msgPayload["id"] = (Json::UInt)decoded->id;
|
||||
msgPayload["name"] = decoded->name;
|
||||
msgPayload["description"] = decoded->description;
|
||||
msgPayload["expire"] = (Json::UInt)decoded->expire;
|
||||
msgPayload["locked_to"] = (Json::UInt)decoded->locked_to;
|
||||
msgPayload["latitude_i"] = (int)decoded->latitude_i;
|
||||
msgPayload["longitude_i"] = (int)decoded->longitude_i;
|
||||
jsonObj["payload"] = msgPayload;
|
||||
} else if (shouldLog) {
|
||||
LOG_ERROR(errStr, msgType.c_str());
|
||||
}
|
||||
@@ -284,26 +289,26 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
|
||||
if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_NeighborInfo_msg,
|
||||
&scratch)) {
|
||||
decoded = &scratch;
|
||||
msgPayload["node_id"] = new JSONValue((unsigned int)decoded->node_id);
|
||||
msgPayload["node_broadcast_interval_secs"] = new JSONValue((unsigned int)decoded->node_broadcast_interval_secs);
|
||||
msgPayload["last_sent_by_id"] = new JSONValue((unsigned int)decoded->last_sent_by_id);
|
||||
msgPayload["neighbors_count"] = new JSONValue(decoded->neighbors_count);
|
||||
JSONArray neighbors;
|
||||
msgPayload["node_id"] = (Json::UInt)decoded->node_id;
|
||||
msgPayload["node_broadcast_interval_secs"] = (Json::UInt)decoded->node_broadcast_interval_secs;
|
||||
msgPayload["last_sent_by_id"] = (Json::UInt)decoded->last_sent_by_id;
|
||||
msgPayload["neighbors_count"] = (Json::UInt)decoded->neighbors_count;
|
||||
Json::Value neighbors(Json::arrayValue);
|
||||
for (uint8_t i = 0; i < decoded->neighbors_count; i++) {
|
||||
JSONObject neighborObj;
|
||||
neighborObj["node_id"] = new JSONValue((unsigned int)decoded->neighbors[i].node_id);
|
||||
neighborObj["snr"] = new JSONValue((int)decoded->neighbors[i].snr);
|
||||
neighbors.push_back(new JSONValue(neighborObj));
|
||||
Json::Value neighborObj(Json::objectValue);
|
||||
neighborObj["node_id"] = (Json::UInt)decoded->neighbors[i].node_id;
|
||||
neighborObj["snr"] = (int)decoded->neighbors[i].snr;
|
||||
neighbors.append(neighborObj);
|
||||
}
|
||||
msgPayload["neighbors"] = new JSONValue(neighbors);
|
||||
jsonObj["payload"] = new JSONValue(msgPayload);
|
||||
msgPayload["neighbors"] = neighbors;
|
||||
jsonObj["payload"] = msgPayload;
|
||||
} else if (shouldLog) {
|
||||
LOG_ERROR(errStr, msgType.c_str());
|
||||
}
|
||||
break;
|
||||
}
|
||||
case meshtastic_PortNum_TRACEROUTE_APP: {
|
||||
if (mp->decoded.request_id) { // Only report the traceroute response
|
||||
if (mp->decoded.request_id) {
|
||||
msgType = "traceroute";
|
||||
meshtastic_RouteDiscovery scratch;
|
||||
meshtastic_RouteDiscovery *decoded = NULL;
|
||||
@@ -311,13 +316,12 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
|
||||
if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_RouteDiscovery_msg,
|
||||
&scratch)) {
|
||||
decoded = &scratch;
|
||||
JSONArray route; // Route this message took
|
||||
JSONArray routeBack; // Route this message took back
|
||||
JSONArray snrTowards; // Snr for forward route
|
||||
JSONArray snrBack; // Snr for reverse route
|
||||
Json::Value route(Json::arrayValue);
|
||||
Json::Value routeBack(Json::arrayValue);
|
||||
Json::Value snrTowards(Json::arrayValue);
|
||||
Json::Value snrBack(Json::arrayValue);
|
||||
|
||||
// Lambda function for adding a long name to the route
|
||||
auto addToRoute = [](JSONArray *route, NodeNum num) {
|
||||
auto addToRoute = [](Json::Value *r, NodeNum num) {
|
||||
char long_name[40] = "Unknown";
|
||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num);
|
||||
bool name_known = nodeInfoLiteHasUser(node);
|
||||
@@ -327,33 +331,32 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
|
||||
memcpy(long_name, node->long_name, copy_len);
|
||||
long_name[copy_len] = '\0';
|
||||
}
|
||||
route->push_back(new JSONValue(long_name));
|
||||
r->append(long_name);
|
||||
};
|
||||
addToRoute(&route, mp->to); // Started at the original transmitter (destination of response)
|
||||
addToRoute(&route, mp->to);
|
||||
for (uint8_t i = 0; i < decoded->route_count; i++) {
|
||||
addToRoute(&route, decoded->route[i]);
|
||||
}
|
||||
addToRoute(&route, mp->from); // Ended at the original destination (source of response)
|
||||
addToRoute(&route, mp->from);
|
||||
|
||||
addToRoute(&routeBack, mp->from); // Started at the original destination (source of response)
|
||||
addToRoute(&routeBack, mp->from);
|
||||
for (uint8_t i = 0; i < decoded->route_back_count; i++) {
|
||||
addToRoute(&routeBack, decoded->route_back[i]);
|
||||
}
|
||||
addToRoute(&routeBack, mp->to); // Ended at the original transmitter (destination of response)
|
||||
addToRoute(&routeBack, mp->to);
|
||||
|
||||
for (uint8_t i = 0; i < decoded->snr_back_count; i++) {
|
||||
snrBack.push_back(new JSONValue((float)decoded->snr_back[i] / 4));
|
||||
snrBack.append((float)decoded->snr_back[i] / 4);
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < decoded->snr_towards_count; i++) {
|
||||
snrTowards.push_back(new JSONValue((float)decoded->snr_towards[i] / 4));
|
||||
snrTowards.append((float)decoded->snr_towards[i] / 4);
|
||||
}
|
||||
|
||||
msgPayload["route"] = new JSONValue(route);
|
||||
msgPayload["route_back"] = new JSONValue(routeBack);
|
||||
msgPayload["snr_back"] = new JSONValue(snrBack);
|
||||
msgPayload["snr_towards"] = new JSONValue(snrTowards);
|
||||
jsonObj["payload"] = new JSONValue(msgPayload);
|
||||
msgPayload["route"] = route;
|
||||
msgPayload["route_back"] = routeBack;
|
||||
msgPayload["snr_back"] = snrBack;
|
||||
msgPayload["snr_towards"] = snrTowards;
|
||||
jsonObj["payload"] = msgPayload;
|
||||
} else if (shouldLog) {
|
||||
LOG_ERROR(errStr, msgType.c_str());
|
||||
}
|
||||
@@ -364,9 +367,9 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
|
||||
msgType = "detection";
|
||||
char payloadStr[(mp->decoded.payload.size) + 1];
|
||||
memcpy(payloadStr, mp->decoded.payload.bytes, mp->decoded.payload.size);
|
||||
payloadStr[mp->decoded.payload.size] = 0; // null terminated string
|
||||
msgPayload["text"] = new JSONValue(payloadStr);
|
||||
jsonObj["payload"] = new JSONValue(msgPayload);
|
||||
payloadStr[mp->decoded.payload.size] = 0;
|
||||
msgPayload["text"] = payloadStr;
|
||||
jsonObj["payload"] = msgPayload;
|
||||
break;
|
||||
}
|
||||
#ifdef ARCH_ESP32
|
||||
@@ -377,10 +380,10 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
|
||||
memset(&scratch, 0, sizeof(scratch));
|
||||
if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Paxcount_msg, &scratch)) {
|
||||
decoded = &scratch;
|
||||
msgPayload["wifi_count"] = new JSONValue((unsigned int)decoded->wifi);
|
||||
msgPayload["ble_count"] = new JSONValue((unsigned int)decoded->ble);
|
||||
msgPayload["uptime"] = new JSONValue((unsigned int)decoded->uptime);
|
||||
jsonObj["payload"] = new JSONValue(msgPayload);
|
||||
msgPayload["wifi_count"] = (Json::UInt)decoded->wifi;
|
||||
msgPayload["ble_count"] = (Json::UInt)decoded->ble;
|
||||
msgPayload["uptime"] = (Json::UInt)decoded->uptime;
|
||||
jsonObj["payload"] = msgPayload;
|
||||
} else if (shouldLog) {
|
||||
LOG_ERROR(errStr, msgType.c_str());
|
||||
}
|
||||
@@ -396,20 +399,19 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
|
||||
decoded = &scratch;
|
||||
if (decoded->type == meshtastic_HardwareMessage_Type_GPIOS_CHANGED) {
|
||||
msgType = "gpios_changed";
|
||||
msgPayload["gpio_value"] = new JSONValue((unsigned int)decoded->gpio_value);
|
||||
jsonObj["payload"] = new JSONValue(msgPayload);
|
||||
msgPayload["gpio_value"] = (Json::UInt)decoded->gpio_value;
|
||||
jsonObj["payload"] = msgPayload;
|
||||
} else if (decoded->type == meshtastic_HardwareMessage_Type_READ_GPIOS_REPLY) {
|
||||
msgType = "gpios_read_reply";
|
||||
msgPayload["gpio_value"] = new JSONValue((unsigned int)decoded->gpio_value);
|
||||
msgPayload["gpio_mask"] = new JSONValue((unsigned int)decoded->gpio_mask);
|
||||
jsonObj["payload"] = new JSONValue(msgPayload);
|
||||
msgPayload["gpio_value"] = (Json::UInt)decoded->gpio_value;
|
||||
msgPayload["gpio_mask"] = (Json::UInt)decoded->gpio_mask;
|
||||
jsonObj["payload"] = msgPayload;
|
||||
}
|
||||
} else if (shouldLog) {
|
||||
LOG_ERROR(errStr, "RemoteHardware");
|
||||
}
|
||||
break;
|
||||
}
|
||||
// add more packet types here if needed
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -417,64 +419,56 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
|
||||
LOG_WARN("Couldn't convert encrypted payload of MeshPacket to JSON");
|
||||
}
|
||||
|
||||
jsonObj["id"] = new JSONValue((unsigned int)mp->id);
|
||||
jsonObj["timestamp"] = new JSONValue((unsigned int)mp->rx_time);
|
||||
jsonObj["to"] = new JSONValue((unsigned int)mp->to);
|
||||
jsonObj["from"] = new JSONValue((unsigned int)mp->from);
|
||||
jsonObj["channel"] = new JSONValue((unsigned int)mp->channel);
|
||||
jsonObj["type"] = new JSONValue(msgType.c_str());
|
||||
jsonObj["sender"] = new JSONValue(nodeDB->getNodeId().c_str());
|
||||
jsonObj["id"] = (Json::UInt)mp->id;
|
||||
jsonObj["timestamp"] = (Json::UInt)mp->rx_time;
|
||||
jsonObj["to"] = (Json::UInt)mp->to;
|
||||
jsonObj["from"] = (Json::UInt)mp->from;
|
||||
jsonObj["channel"] = (Json::UInt)mp->channel;
|
||||
jsonObj["type"] = msgType;
|
||||
jsonObj["sender"] = nodeDB->getNodeId();
|
||||
if (mp->rx_rssi != 0)
|
||||
jsonObj["rssi"] = new JSONValue((int)mp->rx_rssi);
|
||||
jsonObj["rssi"] = (int)mp->rx_rssi;
|
||||
if (mp->rx_snr != 0)
|
||||
jsonObj["snr"] = new JSONValue((float)mp->rx_snr);
|
||||
jsonObj["snr"] = (float)mp->rx_snr;
|
||||
const int8_t hopsAway = getHopsAway(*mp);
|
||||
if (hopsAway >= 0) {
|
||||
jsonObj["hops_away"] = new JSONValue((unsigned int)(hopsAway));
|
||||
jsonObj["hop_start"] = new JSONValue((unsigned int)(mp->hop_start));
|
||||
jsonObj["hops_away"] = (Json::UInt)(hopsAway);
|
||||
jsonObj["hop_start"] = (Json::UInt)(mp->hop_start);
|
||||
}
|
||||
|
||||
// serialize and write it to the stream
|
||||
JSONValue *value = new JSONValue(jsonObj);
|
||||
std::string jsonStr = value->Stringify();
|
||||
std::string jsonStr = writeCompact(jsonObj);
|
||||
|
||||
if (shouldLog)
|
||||
LOG_INFO("serialized json message: %s", jsonStr.c_str());
|
||||
|
||||
delete value;
|
||||
return jsonStr;
|
||||
}
|
||||
|
||||
std::string MeshPacketSerializer::JsonSerializeEncrypted(const meshtastic_MeshPacket *mp)
|
||||
{
|
||||
JSONObject jsonObj;
|
||||
Json::Value jsonObj(Json::objectValue);
|
||||
|
||||
jsonObj["id"] = new JSONValue((unsigned int)mp->id);
|
||||
jsonObj["time_ms"] = new JSONValue((double)millis());
|
||||
jsonObj["timestamp"] = new JSONValue((unsigned int)mp->rx_time);
|
||||
jsonObj["to"] = new JSONValue((unsigned int)mp->to);
|
||||
jsonObj["from"] = new JSONValue((unsigned int)mp->from);
|
||||
jsonObj["channel"] = new JSONValue((unsigned int)mp->channel);
|
||||
jsonObj["want_ack"] = new JSONValue(mp->want_ack);
|
||||
jsonObj["id"] = (Json::UInt)mp->id;
|
||||
jsonObj["time_ms"] = (double)millis();
|
||||
jsonObj["timestamp"] = (Json::UInt)mp->rx_time;
|
||||
jsonObj["to"] = (Json::UInt)mp->to;
|
||||
jsonObj["from"] = (Json::UInt)mp->from;
|
||||
jsonObj["channel"] = (Json::UInt)mp->channel;
|
||||
jsonObj["want_ack"] = mp->want_ack;
|
||||
|
||||
if (mp->rx_rssi != 0)
|
||||
jsonObj["rssi"] = new JSONValue((int)mp->rx_rssi);
|
||||
jsonObj["rssi"] = (int)mp->rx_rssi;
|
||||
if (mp->rx_snr != 0)
|
||||
jsonObj["snr"] = new JSONValue((float)mp->rx_snr);
|
||||
jsonObj["snr"] = (float)mp->rx_snr;
|
||||
const int8_t hopsAway = getHopsAway(*mp);
|
||||
if (hopsAway >= 0) {
|
||||
jsonObj["hops_away"] = new JSONValue((unsigned int)(hopsAway));
|
||||
jsonObj["hop_start"] = new JSONValue((unsigned int)(mp->hop_start));
|
||||
jsonObj["hops_away"] = (Json::UInt)(hopsAway);
|
||||
jsonObj["hop_start"] = (Json::UInt)(mp->hop_start);
|
||||
}
|
||||
jsonObj["size"] = new JSONValue((unsigned int)mp->encrypted.size);
|
||||
jsonObj["size"] = (Json::UInt)mp->encrypted.size;
|
||||
auto encryptedStr = bytesToHex(mp->encrypted.bytes, mp->encrypted.size);
|
||||
jsonObj["bytes"] = new JSONValue(encryptedStr.c_str());
|
||||
jsonObj["bytes"] = encryptedStr;
|
||||
|
||||
// serialize and write it to the stream
|
||||
JSONValue *value = new JSONValue(jsonObj);
|
||||
std::string jsonStr = value->Stringify();
|
||||
|
||||
delete value;
|
||||
return jsonStr;
|
||||
return writeCompact(jsonObj);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,425 +0,0 @@
|
||||
#ifdef NRF52_USE_JSON
|
||||
#warning 'Using nRF52 Serializer'
|
||||
|
||||
#include "ArduinoJson.h"
|
||||
#include "MeshPacketSerializer.h"
|
||||
#include "NodeDB.h"
|
||||
#include "mesh/generated/meshtastic/mqtt.pb.h"
|
||||
#include "mesh/generated/meshtastic/remote_hardware.pb.h"
|
||||
#include "mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "modules/RoutingModule.h"
|
||||
#include <DebugConfiguration.h>
|
||||
#include <mesh-pb-constants.h>
|
||||
|
||||
StaticJsonDocument<1024> jsonObj;
|
||||
StaticJsonDocument<1024> arrayObj;
|
||||
|
||||
std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, bool shouldLog)
|
||||
{
|
||||
// the created jsonObj is immutable after creation, so
|
||||
// we need to do the heavy lifting before assembling it.
|
||||
std::string msgType;
|
||||
jsonObj.clear();
|
||||
arrayObj.clear();
|
||||
|
||||
if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
|
||||
switch (mp->decoded.portnum) {
|
||||
case meshtastic_PortNum_TEXT_MESSAGE_APP: {
|
||||
msgType = "text";
|
||||
// convert bytes to string
|
||||
if (shouldLog)
|
||||
LOG_DEBUG("got text message of size %u", mp->decoded.payload.size);
|
||||
|
||||
char payloadStr[(mp->decoded.payload.size) + 1];
|
||||
memcpy(payloadStr, mp->decoded.payload.bytes, mp->decoded.payload.size);
|
||||
payloadStr[mp->decoded.payload.size] = 0; // null terminated string
|
||||
// check if this is a JSON payload
|
||||
StaticJsonDocument<512> text_doc;
|
||||
DeserializationError error = deserializeJson(text_doc, payloadStr);
|
||||
if (error) {
|
||||
// if it isn't, then we need to create a json object
|
||||
// with the string as the value
|
||||
if (shouldLog)
|
||||
LOG_INFO("text message payload is of type plaintext");
|
||||
jsonObj["payload"]["text"] = payloadStr;
|
||||
} else {
|
||||
// if it is, then we can just use the json object
|
||||
if (shouldLog)
|
||||
LOG_INFO("text message payload is of type json");
|
||||
jsonObj["payload"] = text_doc;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case meshtastic_PortNum_TELEMETRY_APP: {
|
||||
msgType = "telemetry";
|
||||
meshtastic_Telemetry scratch;
|
||||
meshtastic_Telemetry *decoded = NULL;
|
||||
memset(&scratch, 0, sizeof(scratch));
|
||||
if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Telemetry_msg, &scratch)) {
|
||||
decoded = &scratch;
|
||||
if (decoded->which_variant == meshtastic_Telemetry_device_metrics_tag) {
|
||||
// If battery is present, encode the battery level value
|
||||
// TODO - Add a condition to send a code for a non-present value
|
||||
if (decoded->variant.device_metrics.has_battery_level) {
|
||||
jsonObj["payload"]["battery_level"] = (int)decoded->variant.device_metrics.battery_level;
|
||||
}
|
||||
jsonObj["payload"]["voltage"] = decoded->variant.device_metrics.voltage;
|
||||
jsonObj["payload"]["channel_utilization"] = decoded->variant.device_metrics.channel_utilization;
|
||||
jsonObj["payload"]["air_util_tx"] = decoded->variant.device_metrics.air_util_tx;
|
||||
jsonObj["payload"]["uptime_seconds"] = (unsigned int)decoded->variant.device_metrics.uptime_seconds;
|
||||
} else if (decoded->which_variant == meshtastic_Telemetry_environment_metrics_tag) {
|
||||
if (decoded->variant.environment_metrics.has_temperature) {
|
||||
jsonObj["payload"]["temperature"] = decoded->variant.environment_metrics.temperature;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_relative_humidity) {
|
||||
jsonObj["payload"]["relative_humidity"] = decoded->variant.environment_metrics.relative_humidity;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_barometric_pressure) {
|
||||
jsonObj["payload"]["barometric_pressure"] = decoded->variant.environment_metrics.barometric_pressure;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_gas_resistance) {
|
||||
jsonObj["payload"]["gas_resistance"] = decoded->variant.environment_metrics.gas_resistance;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_voltage) {
|
||||
jsonObj["payload"]["voltage"] = decoded->variant.environment_metrics.voltage;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_current) {
|
||||
jsonObj["payload"]["current"] = decoded->variant.environment_metrics.current;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_lux) {
|
||||
jsonObj["payload"]["lux"] = decoded->variant.environment_metrics.lux;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_white_lux) {
|
||||
jsonObj["payload"]["white_lux"] = decoded->variant.environment_metrics.white_lux;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_iaq) {
|
||||
jsonObj["payload"]["iaq"] = (uint)decoded->variant.environment_metrics.iaq;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_wind_speed) {
|
||||
jsonObj["payload"]["wind_speed"] = decoded->variant.environment_metrics.wind_speed;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_wind_direction) {
|
||||
jsonObj["payload"]["wind_direction"] = (uint)decoded->variant.environment_metrics.wind_direction;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_wind_gust) {
|
||||
jsonObj["payload"]["wind_gust"] = decoded->variant.environment_metrics.wind_gust;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_wind_lull) {
|
||||
jsonObj["payload"]["wind_lull"] = decoded->variant.environment_metrics.wind_lull;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_radiation) {
|
||||
jsonObj["payload"]["radiation"] = decoded->variant.environment_metrics.radiation;
|
||||
}
|
||||
} else if (decoded->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) {
|
||||
if (decoded->variant.air_quality_metrics.has_pm10_standard) {
|
||||
jsonObj["payload"]["pm10"] = (unsigned int)decoded->variant.air_quality_metrics.pm10_standard;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_pm25_standard) {
|
||||
jsonObj["payload"]["pm25"] = (unsigned int)decoded->variant.air_quality_metrics.pm25_standard;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_pm100_standard) {
|
||||
jsonObj["payload"]["pm100"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_standard;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_co2) {
|
||||
jsonObj["payload"]["co2"] = (unsigned int)decoded->variant.air_quality_metrics.co2;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_co2_temperature) {
|
||||
jsonObj["payload"]["co2_temperature"] = decoded->variant.air_quality_metrics.co2_temperature;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_co2_humidity) {
|
||||
jsonObj["payload"]["co2_humidity"] = decoded->variant.air_quality_metrics.co2_humidity;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_form_formaldehyde) {
|
||||
jsonObj["payload"]["form_formaldehyde"] = decoded->variant.air_quality_metrics.form_formaldehyde;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_form_temperature) {
|
||||
jsonObj["payload"]["form_temperature"] = decoded->variant.air_quality_metrics.form_temperature;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_form_humidity) {
|
||||
jsonObj["payload"]["form_humidity"] = decoded->variant.air_quality_metrics.form_humidity;
|
||||
}
|
||||
} else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) {
|
||||
if (decoded->variant.power_metrics.has_ch1_voltage) {
|
||||
jsonObj["payload"]["voltage_ch1"] = decoded->variant.power_metrics.ch1_voltage;
|
||||
}
|
||||
if (decoded->variant.power_metrics.has_ch1_current) {
|
||||
jsonObj["payload"]["current_ch1"] = decoded->variant.power_metrics.ch1_current;
|
||||
}
|
||||
if (decoded->variant.power_metrics.has_ch2_voltage) {
|
||||
jsonObj["payload"]["voltage_ch2"] = decoded->variant.power_metrics.ch2_voltage;
|
||||
}
|
||||
if (decoded->variant.power_metrics.has_ch2_current) {
|
||||
jsonObj["payload"]["current_ch2"] = decoded->variant.power_metrics.ch2_current;
|
||||
}
|
||||
if (decoded->variant.power_metrics.has_ch3_voltage) {
|
||||
jsonObj["payload"]["voltage_ch3"] = decoded->variant.power_metrics.ch3_voltage;
|
||||
}
|
||||
if (decoded->variant.power_metrics.has_ch3_current) {
|
||||
jsonObj["payload"]["current_ch3"] = decoded->variant.power_metrics.ch3_current;
|
||||
}
|
||||
}
|
||||
} else if (shouldLog) {
|
||||
LOG_ERROR("Error decoding proto for telemetry message!");
|
||||
return "";
|
||||
}
|
||||
break;
|
||||
}
|
||||
case meshtastic_PortNum_NODEINFO_APP: {
|
||||
msgType = "nodeinfo";
|
||||
meshtastic_User scratch;
|
||||
meshtastic_User *decoded = NULL;
|
||||
memset(&scratch, 0, sizeof(scratch));
|
||||
if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_User_msg, &scratch)) {
|
||||
decoded = &scratch;
|
||||
jsonObj["payload"]["id"] = decoded->id;
|
||||
jsonObj["payload"]["longname"] = decoded->long_name;
|
||||
jsonObj["payload"]["shortname"] = decoded->short_name;
|
||||
jsonObj["payload"]["hardware"] = decoded->hw_model;
|
||||
jsonObj["payload"]["role"] = (int)decoded->role;
|
||||
} else if (shouldLog) {
|
||||
LOG_ERROR("Error decoding proto for nodeinfo message!");
|
||||
return "";
|
||||
}
|
||||
break;
|
||||
}
|
||||
case meshtastic_PortNum_POSITION_APP: {
|
||||
msgType = "position";
|
||||
meshtastic_Position scratch;
|
||||
meshtastic_Position *decoded = NULL;
|
||||
memset(&scratch, 0, sizeof(scratch));
|
||||
if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Position_msg, &scratch)) {
|
||||
decoded = &scratch;
|
||||
if ((int)decoded->time) {
|
||||
jsonObj["payload"]["time"] = (unsigned int)decoded->time;
|
||||
}
|
||||
if ((int)decoded->timestamp) {
|
||||
jsonObj["payload"]["timestamp"] = (unsigned int)decoded->timestamp;
|
||||
}
|
||||
jsonObj["payload"]["latitude_i"] = (int)decoded->latitude_i;
|
||||
jsonObj["payload"]["longitude_i"] = (int)decoded->longitude_i;
|
||||
if ((int)decoded->altitude) {
|
||||
jsonObj["payload"]["altitude"] = (int)decoded->altitude;
|
||||
}
|
||||
if ((int)decoded->ground_speed) {
|
||||
jsonObj["payload"]["ground_speed"] = (unsigned int)decoded->ground_speed;
|
||||
}
|
||||
if (int(decoded->ground_track)) {
|
||||
jsonObj["payload"]["ground_track"] = (unsigned int)decoded->ground_track;
|
||||
}
|
||||
if (int(decoded->sats_in_view)) {
|
||||
jsonObj["payload"]["sats_in_view"] = (unsigned int)decoded->sats_in_view;
|
||||
}
|
||||
if ((int)decoded->PDOP) {
|
||||
jsonObj["payload"]["PDOP"] = (int)decoded->PDOP;
|
||||
}
|
||||
if ((int)decoded->HDOP) {
|
||||
jsonObj["payload"]["HDOP"] = (int)decoded->HDOP;
|
||||
}
|
||||
if ((int)decoded->VDOP) {
|
||||
jsonObj["payload"]["VDOP"] = (int)decoded->VDOP;
|
||||
}
|
||||
if ((int)decoded->precision_bits) {
|
||||
jsonObj["payload"]["precision_bits"] = (int)decoded->precision_bits;
|
||||
}
|
||||
} else if (shouldLog) {
|
||||
LOG_ERROR("Error decoding proto for position message!");
|
||||
return "";
|
||||
}
|
||||
break;
|
||||
}
|
||||
case meshtastic_PortNum_WAYPOINT_APP: {
|
||||
msgType = "position";
|
||||
meshtastic_Waypoint scratch;
|
||||
meshtastic_Waypoint *decoded = NULL;
|
||||
memset(&scratch, 0, sizeof(scratch));
|
||||
if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Waypoint_msg, &scratch)) {
|
||||
decoded = &scratch;
|
||||
jsonObj["payload"]["id"] = (unsigned int)decoded->id;
|
||||
jsonObj["payload"]["name"] = decoded->name;
|
||||
jsonObj["payload"]["description"] = decoded->description;
|
||||
jsonObj["payload"]["expire"] = (unsigned int)decoded->expire;
|
||||
jsonObj["payload"]["locked_to"] = (unsigned int)decoded->locked_to;
|
||||
jsonObj["payload"]["latitude_i"] = (int)decoded->latitude_i;
|
||||
jsonObj["payload"]["longitude_i"] = (int)decoded->longitude_i;
|
||||
} else if (shouldLog) {
|
||||
LOG_ERROR("Error decoding proto for position message!");
|
||||
return "";
|
||||
}
|
||||
break;
|
||||
}
|
||||
case meshtastic_PortNum_NEIGHBORINFO_APP: {
|
||||
msgType = "neighborinfo";
|
||||
meshtastic_NeighborInfo scratch;
|
||||
meshtastic_NeighborInfo *decoded = NULL;
|
||||
memset(&scratch, 0, sizeof(scratch));
|
||||
if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_NeighborInfo_msg,
|
||||
&scratch)) {
|
||||
decoded = &scratch;
|
||||
jsonObj["payload"]["node_id"] = (unsigned int)decoded->node_id;
|
||||
jsonObj["payload"]["node_broadcast_interval_secs"] = (unsigned int)decoded->node_broadcast_interval_secs;
|
||||
jsonObj["payload"]["last_sent_by_id"] = (unsigned int)decoded->last_sent_by_id;
|
||||
jsonObj["payload"]["neighbors_count"] = decoded->neighbors_count;
|
||||
|
||||
JsonObject neighbors_obj = arrayObj.to<JsonObject>();
|
||||
JsonArray neighbors = neighbors_obj.createNestedArray("neighbors");
|
||||
JsonObject neighbors_0 = neighbors.createNestedObject();
|
||||
|
||||
for (uint8_t i = 0; i < decoded->neighbors_count; i++) {
|
||||
neighbors_0["node_id"] = (unsigned int)decoded->neighbors[i].node_id;
|
||||
neighbors_0["snr"] = (int)decoded->neighbors[i].snr;
|
||||
neighbors[i + 1] = neighbors_0;
|
||||
neighbors_0.clear();
|
||||
}
|
||||
neighbors.remove(0);
|
||||
jsonObj["payload"]["neighbors"] = neighbors;
|
||||
} else if (shouldLog) {
|
||||
LOG_ERROR("Error decoding proto for neighborinfo message!");
|
||||
return "";
|
||||
}
|
||||
break;
|
||||
}
|
||||
case meshtastic_PortNum_TRACEROUTE_APP: {
|
||||
if (mp->decoded.request_id) { // Only report the traceroute response
|
||||
msgType = "traceroute";
|
||||
meshtastic_RouteDiscovery scratch;
|
||||
meshtastic_RouteDiscovery *decoded = NULL;
|
||||
memset(&scratch, 0, sizeof(scratch));
|
||||
if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_RouteDiscovery_msg,
|
||||
&scratch)) {
|
||||
decoded = &scratch;
|
||||
JsonArray route = arrayObj.createNestedArray("route");
|
||||
|
||||
auto addToRoute = [](JsonArray *route, NodeNum num) {
|
||||
char long_name[40] = "Unknown";
|
||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num);
|
||||
bool name_known = nodeInfoLiteHasUser(node);
|
||||
if (name_known) {
|
||||
const size_t copy_len =
|
||||
(sizeof(node->long_name) < sizeof(long_name)) ? sizeof(node->long_name) : sizeof(long_name) - 1;
|
||||
memcpy(long_name, node->long_name, copy_len);
|
||||
long_name[copy_len] = '\0';
|
||||
}
|
||||
route->add(long_name);
|
||||
};
|
||||
|
||||
addToRoute(&route, mp->to); // route.add(mp->to);
|
||||
for (uint8_t i = 0; i < decoded->route_count; i++) {
|
||||
addToRoute(&route, decoded->route[i]); // route.add(decoded->route[i]);
|
||||
}
|
||||
addToRoute(&route,
|
||||
mp->from); // route.add(mp->from); // Ended at the original destination (source of response)
|
||||
|
||||
jsonObj["payload"]["route"] = route;
|
||||
} else if (shouldLog) {
|
||||
LOG_ERROR("Error decoding proto for traceroute message!");
|
||||
return "";
|
||||
}
|
||||
} else {
|
||||
LOG_WARN("Traceroute response not reported");
|
||||
return "";
|
||||
}
|
||||
break;
|
||||
}
|
||||
case meshtastic_PortNum_DETECTION_SENSOR_APP: {
|
||||
msgType = "detection";
|
||||
char payloadStr[(mp->decoded.payload.size) + 1];
|
||||
memcpy(payloadStr, mp->decoded.payload.bytes, mp->decoded.payload.size);
|
||||
payloadStr[mp->decoded.payload.size] = 0; // null terminated string
|
||||
jsonObj["payload"]["text"] = payloadStr;
|
||||
break;
|
||||
}
|
||||
case meshtastic_PortNum_REMOTE_HARDWARE_APP: {
|
||||
meshtastic_HardwareMessage scratch;
|
||||
meshtastic_HardwareMessage *decoded = NULL;
|
||||
memset(&scratch, 0, sizeof(scratch));
|
||||
if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_HardwareMessage_msg,
|
||||
&scratch)) {
|
||||
decoded = &scratch;
|
||||
if (decoded->type == meshtastic_HardwareMessage_Type_GPIOS_CHANGED) {
|
||||
msgType = "gpios_changed";
|
||||
jsonObj["payload"]["gpio_value"] = (unsigned int)decoded->gpio_value;
|
||||
} else if (decoded->type == meshtastic_HardwareMessage_Type_READ_GPIOS_REPLY) {
|
||||
msgType = "gpios_read_reply";
|
||||
jsonObj["payload"]["gpio_value"] = (unsigned int)decoded->gpio_value;
|
||||
jsonObj["payload"]["gpio_mask"] = (unsigned int)decoded->gpio_mask;
|
||||
}
|
||||
} else if (shouldLog) {
|
||||
LOG_ERROR("Error decoding proto for RemoteHardware message!");
|
||||
return "";
|
||||
}
|
||||
break;
|
||||
}
|
||||
// add more packet types here if needed
|
||||
default:
|
||||
LOG_WARN("Unsupported packet type %d", mp->decoded.portnum);
|
||||
return "";
|
||||
break;
|
||||
}
|
||||
} else if (shouldLog) {
|
||||
LOG_WARN("Couldn't convert encrypted payload of MeshPacket to JSON");
|
||||
return "";
|
||||
}
|
||||
|
||||
jsonObj["id"] = (unsigned int)mp->id;
|
||||
jsonObj["timestamp"] = (unsigned int)mp->rx_time;
|
||||
jsonObj["to"] = (unsigned int)mp->to;
|
||||
jsonObj["from"] = (unsigned int)mp->from;
|
||||
jsonObj["channel"] = (unsigned int)mp->channel;
|
||||
jsonObj["type"] = msgType.c_str();
|
||||
jsonObj["sender"] = nodeDB->getNodeId().c_str();
|
||||
if (mp->rx_rssi != 0)
|
||||
jsonObj["rssi"] = (int)mp->rx_rssi;
|
||||
if (mp->rx_snr != 0)
|
||||
jsonObj["snr"] = (float)mp->rx_snr;
|
||||
const int8_t hopsAway = getHopsAway(*mp);
|
||||
if (hopsAway >= 0) {
|
||||
jsonObj["hops_away"] = (unsigned int)(hopsAway);
|
||||
jsonObj["hop_start"] = (unsigned int)(mp->hop_start);
|
||||
}
|
||||
|
||||
// serialize and write it to the stream
|
||||
|
||||
// Serial.printf("serialized json message: \r");
|
||||
// serializeJson(jsonObj, Serial);
|
||||
// Serial.println("");
|
||||
|
||||
std::string jsonStr = "";
|
||||
serializeJson(jsonObj, jsonStr);
|
||||
|
||||
if (shouldLog)
|
||||
LOG_INFO("serialized json message: %s", jsonStr.c_str());
|
||||
|
||||
return jsonStr;
|
||||
}
|
||||
|
||||
std::string MeshPacketSerializer::JsonSerializeEncrypted(const meshtastic_MeshPacket *mp)
|
||||
{
|
||||
jsonObj.clear();
|
||||
jsonObj["id"] = (unsigned int)mp->id;
|
||||
jsonObj["time_ms"] = (double)millis();
|
||||
jsonObj["timestamp"] = (unsigned int)mp->rx_time;
|
||||
jsonObj["to"] = (unsigned int)mp->to;
|
||||
jsonObj["from"] = (unsigned int)mp->from;
|
||||
jsonObj["channel"] = (unsigned int)mp->channel;
|
||||
jsonObj["want_ack"] = mp->want_ack;
|
||||
|
||||
if (mp->rx_rssi != 0)
|
||||
jsonObj["rssi"] = (int)mp->rx_rssi;
|
||||
if (mp->rx_snr != 0)
|
||||
jsonObj["snr"] = (float)mp->rx_snr;
|
||||
const int8_t hopsAway = getHopsAway(*mp);
|
||||
if (hopsAway >= 0) {
|
||||
jsonObj["hops_away"] = (unsigned int)(hopsAway);
|
||||
jsonObj["hop_start"] = (unsigned int)(mp->hop_start);
|
||||
}
|
||||
jsonObj["size"] = (unsigned int)mp->encrypted.size;
|
||||
auto encryptedStr = bytesToHex(mp->encrypted.bytes, mp->encrypted.size);
|
||||
jsonObj["bytes"] = encryptedStr.c_str();
|
||||
|
||||
// serialize and write it to the stream
|
||||
std::string jsonStr = "";
|
||||
serializeJson(jsonObj, jsonStr);
|
||||
|
||||
return jsonStr;
|
||||
}
|
||||
#endif
|
||||
+2
-2
@@ -234,8 +234,8 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN
|
||||
|
||||
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_BLUETOOTH
|
||||
// Full shutdown of bluetooth hardware
|
||||
if (nimbleBluetooth)
|
||||
nimbleBluetooth->deinit();
|
||||
if (bluetoothApi)
|
||||
bluetoothApi->deinit();
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
|
||||
+88
-14
@@ -15,6 +15,38 @@ pio test -e native -f test_your_module
|
||||
pio test -e native -f test_your_module -vvv
|
||||
```
|
||||
|
||||
**Never pipe through `| tail -N` to shorten output.** PlatformIO prints build errors at the top of output and test results at the bottom; `tail` will show stale cached results from a prior successful build while hiding the compile error that caused the current run to fail.
|
||||
|
||||
**Preferred pattern — redirect to file, then grep:**
|
||||
|
||||
```bash
|
||||
# Redirect all output to a file; grep for errors and results after it exits
|
||||
pio test -e native -f test_your_module > /tmp/test_out.txt 2>&1
|
||||
echo "exit: $?"
|
||||
grep -E 'error:|PASS|FAIL|succeeded|failed' /tmp/test_out.txt
|
||||
tail -15 /tmp/test_out.txt
|
||||
```
|
||||
|
||||
Why: piping through `| grep` line-buffers the output and suppresses all progress until the process exits, making it look hung. The redirect approach lets the build stream normally while still giving you filtered results afterwards.
|
||||
|
||||
**Viewing verbose test output without truncation (e.g. `TEST_MESSAGE` group headers):**
|
||||
|
||||
```bash
|
||||
/tmp/meshtastic-pio-venv/bin/python -m platformio test -e coverage --filter test_mesh_beacon -vv 2>&1 | grep -v "[[:space:]]SKIPPED$"
|
||||
```
|
||||
|
||||
The `-vv` flag makes Unity emit `INFO:` lines from `TEST_MESSAGE` calls; piping through `grep -v SKIPPED` removes the noise from platform feature gates while keeping all PASS/FAIL/INFO lines visible.
|
||||
|
||||
**`externally-managed-environment` error on Ubuntu/Debian:**
|
||||
|
||||
If `pio test` fails immediately with `error: externally-managed-environment`, the system `pio` binary is using the OS Python which newer distros lock down. Use PlatformIO's own venv instead:
|
||||
|
||||
```bash
|
||||
~/.platformio/penv/bin/python -m platformio test -e native -f test_your_module > /tmp/test_out.txt 2>&1
|
||||
grep -E 'error:|PASS|FAIL|succeeded|failed' /tmp/test_out.txt
|
||||
tail -15 /tmp/test_out.txt
|
||||
```
|
||||
|
||||
### Helper Scripts (Useful Shortcuts)
|
||||
|
||||
These wrappers are handy when local host dependencies are missing or when you want repeatable commands.
|
||||
@@ -82,12 +114,15 @@ One file per suite. No per-test `platformio.ini` is needed — tests build under
|
||||
#include "gps/RTC.h"
|
||||
#include "mesh/NodeDB.h"
|
||||
#include "modules/YourModule.h"
|
||||
#include <cstdio>
|
||||
#include <cstdio> // required for printf() — used for blank-line group separators
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
|
||||
// --- Test output helpers ---
|
||||
// Unity swallows printf/stdout. Only TEST_MESSAGE() output appears in results.
|
||||
// printf() writes directly to stdout and appears in -vv output as a plain line (no prefix).
|
||||
// Use it for blank-line group separators: printf("\n");
|
||||
// TEST_MESSAGE() emits a "file:line:INFO: <text>" line — visible at -vv and above.
|
||||
// Use TEST_MSG_FMT for formatted diagnostic lines inside tests.
|
||||
#define MSG_BUF_LEN 200
|
||||
#define TEST_MSG_FMT(fmt, ...) do { \
|
||||
char _buf[MSG_BUF_LEN]; \
|
||||
@@ -112,6 +147,9 @@ void setup()
|
||||
{
|
||||
initializeTestEnvironment(); // MUST call — sets up RTC, OSThread, console
|
||||
UNITY_BEGIN();
|
||||
|
||||
printf("\n=== Example group ===\n"); // header line to help find tests
|
||||
|
||||
RUN_TEST(test_example);
|
||||
exit(UNITY_END()); // exit() required — Unity runner expects it
|
||||
}
|
||||
@@ -162,7 +200,7 @@ class MockNodeDB : public NodeDB
|
||||
node.num = num;
|
||||
node.has_hops_away = hasHops;
|
||||
node.hops_away = hopsAway;
|
||||
node.via_mqtt = viaMqtt;
|
||||
nodeInfoLiteSetBit(&node, NODEINFO_BITFIELD_VIA_MQTT_MASK, viaMqtt);
|
||||
node.last_heard = getTime() - ageSecs;
|
||||
testNodes.push_back(node);
|
||||
meshNodes = &testNodes;
|
||||
@@ -185,20 +223,34 @@ Subclass the module under test to make protected methods callable and private me
|
||||
class YourModuleTestShim : public YourModule
|
||||
{
|
||||
public:
|
||||
// Expose protected methods
|
||||
// Pull protected methods into public scope via using.
|
||||
// IMPORTANT: using requires the method to be protected (or public) in the base —
|
||||
// friend alone does NOT satisfy this. See pitfall #6.
|
||||
using YourModule::runOnce;
|
||||
using YourModule::someProtectedMethod;
|
||||
|
||||
// Access private members via friend (see below)
|
||||
// Wrap private members with setter methods (friend grants direct access here).
|
||||
void setPrivateField(int x) { privateField = x; }
|
||||
};
|
||||
```
|
||||
|
||||
In the module header, grant friend access under the `UNIT_TEST` define (set automatically by PlatformIO's test framework):
|
||||
For methods you want to expose via `using`, use the conditional access-specifier pattern in the header — **not** plain `friend`:
|
||||
|
||||
```cpp
|
||||
// In YourModule.h, inside the class body:
|
||||
#ifdef UNIT_TEST
|
||||
#ifdef PIO_UNIT_TESTING
|
||||
protected:
|
||||
#else
|
||||
private:
|
||||
#endif
|
||||
bool someMethod();
|
||||
```
|
||||
|
||||
For private _member variables_ that a shim setter needs to touch directly, `friend` is sufficient (no `using` involved):
|
||||
|
||||
```cpp
|
||||
// In YourModule.h, inside the class body:
|
||||
#ifdef PIO_UNIT_TESTING
|
||||
friend class YourModuleTestShim;
|
||||
#endif
|
||||
```
|
||||
@@ -284,6 +336,21 @@ Fixed-size data structures (hash sets, ring buffers) overflow when tests inject
|
||||
|
||||
**Fix:** Simulate multiple realistic time windows rather than one massive burst. Let adaptive mechanisms (if any) self-tune over several rolls.
|
||||
|
||||
### 6. Granting test access to private/protected members
|
||||
|
||||
PlatformIO defines `PIO_UNIT_TESTING` during `pio test` builds. Several production headers (`TransmitHistory.h`, `CryptoEngine.h`, `MQTT.h`, `RTC.h`) use this to gate test-only visibility changes. PlatformIO also defines `UNIT_TEST` in the same builds for backward compatibility, but that spelling is deprecated — always use `PIO_UNIT_TESTING` in new code. The established pattern for exposing a private method to a test shim **without widening production visibility**:
|
||||
|
||||
```cpp
|
||||
#ifdef PIO_UNIT_TESTING
|
||||
protected:
|
||||
#else
|
||||
private:
|
||||
#endif
|
||||
bool myMethod();
|
||||
```
|
||||
|
||||
**Critical C++ rule:** a `using` declaration in a derived class (e.g. `using Base::myMethod`) requires `myMethod` to be `protected` or `public` in the base — `friend` alone does **not** satisfy this. Adding `friend class TestShim` while leaving the method `private` will still fail to compile. Use the conditional access-specifier pattern above, not `friend`.
|
||||
|
||||
## setUp/tearDown Checklist
|
||||
|
||||
- [ ] Create and clear MockNodeDB (if needed)
|
||||
@@ -291,6 +358,7 @@ Fixed-size data structures (hash sets, ring buffers) overflow when tests inject
|
||||
- [ ] Set `nodeDB = mockNodeDB`
|
||||
- [ ] Delete persisted state files (`FSCom.remove(...)`)
|
||||
- [ ] Reset file-scope mutable globals
|
||||
- [ ] Reset mock clock to a safe base value (e.g. `mockTime = ONE_HOUR_MS`) — prevents unsigned subtraction underflow in time-dependent logic
|
||||
- [ ] Disable randomness/jitter flags
|
||||
- [ ] In `tearDown`: null the global singleton pointer, restore flags
|
||||
|
||||
@@ -308,15 +376,21 @@ A well-structured test suite follows this pattern:
|
||||
|
||||
| Suite | Module Under Test |
|
||||
| ---------------------------- | ----------------------------- |
|
||||
| `test_admin_radio` | Admin + LoRa region config |
|
||||
| `test_atak` | ATAK integration |
|
||||
| `test_crypto` | CryptoEngine |
|
||||
| `test_mqtt` | MQTT integration |
|
||||
| `test_radio` | Radio interface |
|
||||
| `test_default` | Default configuration helpers |
|
||||
| `test_hop_scaling` | Hop scaling algorithm |
|
||||
| `test_http_content_handler` | HTTP handling |
|
||||
| `test_mac_from_string` | MAC address parsing |
|
||||
| `test_mesh_module` | Module framework |
|
||||
| `test_meshpacket_serializer` | Packet serialization |
|
||||
| `test_transmit_history` | Retransmission tracking |
|
||||
| `test_atak` | ATAK integration |
|
||||
| `test_default` | Default configuration helpers |
|
||||
| `test_http_content_handler` | HTTP handling |
|
||||
| `test_mqtt` | MQTT integration |
|
||||
| `test_packet_history` | Packet history tracking |
|
||||
| `test_position_precision` | Position precision helpers |
|
||||
| `test_radio` | Radio interface |
|
||||
| `test_serial` | Serial communication |
|
||||
| `test_hop_scaling` | Hop scaling algorithm |
|
||||
| `test_traffic_management` | Traffic management |
|
||||
| `test_transmit_history` | Retransmission tracking |
|
||||
| `test_type_conversions` | NodeDB v25 type conversions |
|
||||
| `test_utf8` | UTF-8 utilities |
|
||||
|
||||
@@ -36,7 +36,6 @@ static MockMeshService *mockMeshService;
|
||||
// -----------------------------------------------------------------------
|
||||
// getRegion() tests
|
||||
// -----------------------------------------------------------------------
|
||||
extern const RegionInfo *getRegion(meshtastic_Config_LoRaConfig_RegionCode code);
|
||||
|
||||
static void test_getRegion_returnsCorrectRegion_US()
|
||||
{
|
||||
@@ -104,6 +103,29 @@ static void test_validateConfigRegion_unsetRegionReturnsTrue()
|
||||
TEST_ASSERT_TRUE(RadioInterface::validateConfigRegion(cfg));
|
||||
}
|
||||
|
||||
static void test_validateConfigRegion_unknownCodeReturnsFalse()
|
||||
{
|
||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
||||
cfg.region = (meshtastic_Config_LoRaConfig_RegionCode)255;
|
||||
|
||||
devicestate.owner.is_licensed = false;
|
||||
|
||||
// Unknown code is not in the regions table; getRegion() returns the UNSET sentinel,
|
||||
// whose .code != 255, so validateConfigRegion should reject it.
|
||||
TEST_ASSERT_FALSE(RadioInterface::validateConfigRegion(cfg));
|
||||
}
|
||||
|
||||
static void test_validateConfigRegion_anotherUnknownCodeReturnsFalse()
|
||||
{
|
||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
||||
cfg.region = (meshtastic_Config_LoRaConfig_RegionCode)99;
|
||||
|
||||
devicestate.owner.is_licensed = true;
|
||||
|
||||
// Unknown code should be rejected even when owner is licensed.
|
||||
TEST_ASSERT_FALSE(RadioInterface::validateConfigRegion(cfg));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Shadow tables for testing (preset lists → profiles → regions → lookup)
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -937,6 +959,8 @@ void setup()
|
||||
// validateConfigRegion()
|
||||
RUN_TEST(test_validateConfigRegion_validRegionReturnsTrue);
|
||||
RUN_TEST(test_validateConfigRegion_unsetRegionReturnsTrue);
|
||||
RUN_TEST(test_validateConfigRegion_unknownCodeReturnsFalse);
|
||||
RUN_TEST(test_validateConfigRegion_anotherUnknownCodeReturnsFalse);
|
||||
|
||||
// Shadow table tests
|
||||
RUN_TEST(test_shadowTable_spacedProfileHasNonZeroSpacing);
|
||||
|
||||
@@ -0,0 +1,738 @@
|
||||
#include "MeshTypes.h"
|
||||
#include "TestUtil.h"
|
||||
#include <unity.h>
|
||||
|
||||
#if HAS_VARIABLE_HOPS
|
||||
|
||||
#include "FSCommon.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "mesh/NodeDB.h"
|
||||
#include "modules/HopScalingModule.h"
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
|
||||
// Unity only shows TEST_MESSAGE output. printf goes to stdout which the runner swallows.
|
||||
#define MSG_BUF_LEN 200
|
||||
#define TEST_MSG_FMT(fmt, ...) \
|
||||
do { \
|
||||
char _buf[MSG_BUF_LEN]; \
|
||||
snprintf(_buf, sizeof(_buf), fmt, __VA_ARGS__); \
|
||||
TEST_MESSAGE(_buf); \
|
||||
} while (0)
|
||||
|
||||
static constexpr NodeNum kLocalNode = 0x11111111;
|
||||
|
||||
// Shared mock clock — drives HopScalingModule::nowMs()
|
||||
static uint32_t &mockTime = HopScalingModule::s_testNowMs;
|
||||
static constexpr uint32_t ONE_HOUR_MS = 3600UL * 1000UL;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MockNodeDB — not used for hop decisions any more, kept for completeness
|
||||
// ---------------------------------------------------------------------------
|
||||
class MockNodeDB : public NodeDB
|
||||
{
|
||||
public:
|
||||
void clearTestNodes()
|
||||
{
|
||||
testNodes.clear();
|
||||
numMeshNodes = 0;
|
||||
}
|
||||
|
||||
void addTestNode(NodeNum num, uint8_t hopsAway, bool hasHops, uint32_t ageSecs, bool viaMqtt = false)
|
||||
{
|
||||
meshtastic_NodeInfoLite node = meshtastic_NodeInfoLite_init_zero;
|
||||
node.num = num;
|
||||
node.has_hops_away = hasHops;
|
||||
node.hops_away = hopsAway;
|
||||
nodeInfoLiteSetBit(&node, NODEINFO_BITFIELD_VIA_MQTT_MASK, viaMqtt);
|
||||
node.last_heard = getTime() - ageSecs;
|
||||
testNodes.push_back(node);
|
||||
meshNodes = &testNodes;
|
||||
numMeshNodes = testNodes.size();
|
||||
}
|
||||
|
||||
std::vector<meshtastic_NodeInfoLite> testNodes;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test shim — expose protected/private members for direct invocation
|
||||
// ---------------------------------------------------------------------------
|
||||
class HopScalingTestShim : public HopScalingModule
|
||||
{
|
||||
public:
|
||||
using HopScalingModule::runOnce;
|
||||
using HopScalingModule::samplePacketForHistogram;
|
||||
|
||||
using HopScalingModule::getLastRequiredHop;
|
||||
|
||||
// Test-only helpers (require UNIT_TEST friend access)
|
||||
void rollHourTest() { rollHour(); }
|
||||
void setHistogramDenominator(uint8_t d) { setSamplingDenominator(d); }
|
||||
|
||||
/// Directly set denominator state, bypassing any scale-up/down logic.
|
||||
/// Used by tests that need a specific pre-condition without triggering trim.
|
||||
void forceFilterDenomState(uint8_t samp, uint8_t filt, uint8_t holdRolls)
|
||||
{
|
||||
samplingDenominator = samp;
|
||||
filteringDenominator = filt;
|
||||
filteringDenomHoldRollsRemaining = holdRolls;
|
||||
}
|
||||
uint8_t getFilteringDenomHoldRollsRemaining() const { return filteringDenomHoldRollsRemaining; }
|
||||
|
||||
/// Insert an entry with an explicit hash, bypassing the sampling filter.
|
||||
/// Used to fill the histogram to a known state without depending on hashNodeId distribution.
|
||||
void forceInsertEntry(uint16_t hash, uint8_t hops)
|
||||
{
|
||||
if (count < CAPACITY) {
|
||||
entries[count].nodeHash = hash;
|
||||
entries[count].hops_away = hops;
|
||||
entries[count].seenHoursAgo = 1u;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Size introspection for test_memory_layout
|
||||
static constexpr size_t sizeofSelf() { return sizeof(HopScalingModule); }
|
||||
};
|
||||
|
||||
static MockNodeDB *mockNodeDB = nullptr;
|
||||
|
||||
// Create deterministic IDs that produce a broad spread of 16-bit hashes.
|
||||
// HopScalingModule admission uses passesFilter(hashNodeId(nodeId), denom), NOT a raw nodeId
|
||||
// modulo check — do not assume (nodeId & (denom-1)) == 0 determines whether a node is admitted.
|
||||
static uint32_t makeDistributedNodeId(uint32_t baseId, uint32_t ordinal, uint32_t salt = 0)
|
||||
{
|
||||
return baseId + salt + (ordinal * 33u);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers — mesh topology builders
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Helper: add N nodes at a given hop with ages spread across a time range.
|
||||
static void addNodesAtHop(uint32_t baseId, uint8_t hop, uint32_t count, uint32_t ageSecs, uint32_t stride = 10)
|
||||
{
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
const uint32_t nodeId = makeDistributedNodeId(baseId, i, static_cast<uint32_t>(hop) << 8);
|
||||
mockNodeDB->addTestNode(nodeId, hop, true, ageSecs + i * stride);
|
||||
}
|
||||
}
|
||||
|
||||
// Feed sampled traffic into the histogram.
|
||||
// Advances mock clock by one hour per roll and calls rollHour() so each roll produces data.
|
||||
static void injectSampleTraffic(HopScalingTestShim &shim, uint32_t baseId, const uint16_t hopDist[HOP_MAX + 1],
|
||||
uint8_t numRolls = 16)
|
||||
{
|
||||
shim.setHistogramDenominator(HopScalingModule::DENOM_MIN);
|
||||
|
||||
for (uint8_t roll = 0; roll < numRolls; ++roll) {
|
||||
mockTime += ONE_HOUR_MS;
|
||||
|
||||
uint16_t ordinal = 0;
|
||||
for (uint8_t hop = 0; hop <= HOP_MAX; ++hop) {
|
||||
for (uint16_t n = 0; n < hopDist[hop]; ++n) {
|
||||
const uint32_t nodeId = makeDistributedNodeId(baseId, ordinal);
|
||||
shim.samplePacketForHistogram(nodeId, hop);
|
||||
++ordinal;
|
||||
}
|
||||
}
|
||||
shim.rollHourTest();
|
||||
}
|
||||
}
|
||||
|
||||
static void assertCompactHistogramActive(HopScalingTestShim &shim)
|
||||
{
|
||||
TEST_ASSERT_GREATER_THAN_UINT8(0, shim.getCompactHistogramEntryCount());
|
||||
TEST_ASSERT_TRUE(shim.getCompactHistogramAllSampleCount() > 0);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Topology builders
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Scenario A: Dense local mesh — 110 nodes, heavy at hops 0–2.
|
||||
static void buildDenseLocalMesh()
|
||||
{
|
||||
mockNodeDB->clearTestNodes();
|
||||
addNodesAtHop(0x1000, 0, 25, 120);
|
||||
addNodesAtHop(0x2000, 1, 30, 300);
|
||||
addNodesAtHop(0x3000, 2, 15, 600);
|
||||
addNodesAtHop(0x4000, 3, 5, 1200);
|
||||
addNodesAtHop(0x5000, 4, 10, 1800);
|
||||
addNodesAtHop(0x6000, 5, 15, 2400);
|
||||
addNodesAtHop(0x7000, 6, 10, 3000);
|
||||
}
|
||||
|
||||
// Scenario B: Spread sparse mesh — 76 nodes across hops 0–7.
|
||||
static void buildSpreadSparseMesh()
|
||||
{
|
||||
mockNodeDB->clearTestNodes();
|
||||
addNodesAtHop(0x1000, 0, 5, 120);
|
||||
addNodesAtHop(0x2000, 1, 8, 300);
|
||||
addNodesAtHop(0x3000, 2, 12, 600);
|
||||
addNodesAtHop(0x4000, 3, 15, 900);
|
||||
addNodesAtHop(0x5000, 4, 10, 1200);
|
||||
addNodesAtHop(0x6000, 5, 6, 1800);
|
||||
addNodesAtHop(0x7000, 6, 10, 3000);
|
||||
addNodesAtHop(0x8000, 7, 10, 3600);
|
||||
}
|
||||
|
||||
// Scenario C: Deep linear chain — 22 thin nodes, never reaches 40.
|
||||
static void buildDeepLinearChain()
|
||||
{
|
||||
mockNodeDB->clearTestNodes();
|
||||
addNodesAtHop(0x1000, 0, 2, 120);
|
||||
addNodesAtHop(0x2000, 1, 3, 300);
|
||||
addNodesAtHop(0x3000, 2, 3, 600);
|
||||
addNodesAtHop(0x4000, 3, 4, 900);
|
||||
addNodesAtHop(0x5000, 4, 3, 1200);
|
||||
addNodesAtHop(0x6000, 5, 2, 1800);
|
||||
addNodesAtHop(0x7000, 6, 2, 2400);
|
||||
addNodesAtHop(0x8000, 7, 3, 3600);
|
||||
}
|
||||
|
||||
// Scenario D: Router cluster — 71 nodes, 45 at hop 2.
|
||||
static void buildRouterCluster()
|
||||
{
|
||||
mockNodeDB->clearTestNodes();
|
||||
addNodesAtHop(0x1000, 0, 3, 120);
|
||||
addNodesAtHop(0x2000, 1, 5, 300);
|
||||
addNodesAtHop(0x3000, 2, 45, 600);
|
||||
addNodesAtHop(0x4000, 3, 8, 1200);
|
||||
addNodesAtHop(0x5000, 4, 3, 1200);
|
||||
addNodesAtHop(0x6000, 5, 2, 1800);
|
||||
addNodesAtHop(0x7000, 6, 2, 2400);
|
||||
addNodesAtHop(0x8000, 7, 3, 3600);
|
||||
}
|
||||
|
||||
// Scenario E: Megamesh — 199 nodes (DB near capacity).
|
||||
static void buildMegamesh()
|
||||
{
|
||||
mockNodeDB->clearTestNodes();
|
||||
addNodesAtHop(0x01000, 0, 30, 120);
|
||||
addNodesAtHop(0x02000, 1, 40, 300);
|
||||
addNodesAtHop(0x03000, 2, 35, 600);
|
||||
addNodesAtHop(0x04000, 3, 30, 900);
|
||||
addNodesAtHop(0x05000, 4, 20, 1200);
|
||||
addNodesAtHop(0x06000, 5, 15, 1800);
|
||||
addNodesAtHop(0x07000, 6, 14, 2400);
|
||||
addNodesAtHop(0x08000, 7, 15, 3600);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests — Topology-driven hop reduction scenarios
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void test_dense_local_telemetry()
|
||||
{
|
||||
TEST_MESSAGE("=== Dense local mesh: telemetry broadcast ===");
|
||||
TEST_MESSAGE("Topology: 110 nodes with 25/30/15 nodes at hops 0/1/2 and a thinner tail to hop 6.");
|
||||
TEST_MESSAGE("Expectation: cumulative reaches 55 nodes by hop 1, result stays tightly constrained.");
|
||||
|
||||
auto shim = std::unique_ptr<HopScalingTestShim>(new HopScalingTestShim());
|
||||
hopScalingModule = shim.get();
|
||||
buildDenseLocalMesh();
|
||||
const uint16_t distA[HOP_MAX + 1] = {25, 30, 15, 5, 10, 15, 10, 0};
|
||||
injectSampleTraffic(*shim, 0x91000000, distA);
|
||||
shim->runOnce();
|
||||
|
||||
TEST_MSG_FMT("Dense local: hop=%u", shim->getLastRequiredHop());
|
||||
TEST_ASSERT_TRUE(shim->getLastRequiredHop() <= 3);
|
||||
TEST_ASSERT_TRUE(shim->getLastRequiredHop() >= 1);
|
||||
assertCompactHistogramActive(*shim);
|
||||
|
||||
hopScalingModule = nullptr;
|
||||
}
|
||||
|
||||
void test_spread_sparse_position()
|
||||
{
|
||||
TEST_MESSAGE("=== Spread sparse mesh: position broadcast ===");
|
||||
TEST_MESSAGE("Topology: 76 nodes spread across all hops, reaching 40 nodes only when hop 3 is included.");
|
||||
TEST_MESSAGE("Expectation: hop settles in the 3-5 range.");
|
||||
|
||||
auto shim = std::unique_ptr<HopScalingTestShim>(new HopScalingTestShim());
|
||||
hopScalingModule = shim.get();
|
||||
buildSpreadSparseMesh();
|
||||
const uint16_t distB[HOP_MAX + 1] = {5, 8, 12, 15, 10, 6, 10, 10};
|
||||
injectSampleTraffic(*shim, 0x92000000, distB);
|
||||
shim->runOnce();
|
||||
|
||||
TEST_MSG_FMT("Spread sparse: hop=%u", shim->getLastRequiredHop());
|
||||
TEST_ASSERT_TRUE(shim->getLastRequiredHop() >= 3);
|
||||
TEST_ASSERT_TRUE(shim->getLastRequiredHop() <= 5);
|
||||
assertCompactHistogramActive(*shim);
|
||||
|
||||
hopScalingModule = nullptr;
|
||||
}
|
||||
|
||||
void test_deep_chain_position()
|
||||
{
|
||||
TEST_MESSAGE("=== Deep linear chain: position broadcast ===");
|
||||
TEST_MESSAGE("Topology: 22 nodes spread thinly across hops 0-7, never reaching the 40-node floor.");
|
||||
TEST_MESSAGE("Expectation: module must keep HOP_MAX.");
|
||||
|
||||
auto shim = std::unique_ptr<HopScalingTestShim>(new HopScalingTestShim());
|
||||
hopScalingModule = shim.get();
|
||||
buildDeepLinearChain();
|
||||
const uint16_t distC[HOP_MAX + 1] = {2, 3, 3, 4, 3, 2, 2, 3};
|
||||
injectSampleTraffic(*shim, 0x93000000, distC);
|
||||
shim->runOnce();
|
||||
|
||||
TEST_MSG_FMT("Deep chain: hop=%u", shim->getLastRequiredHop());
|
||||
TEST_ASSERT_EQUAL_UINT8(HOP_MAX, shim->getLastRequiredHop());
|
||||
assertCompactHistogramActive(*shim);
|
||||
|
||||
hopScalingModule = nullptr;
|
||||
}
|
||||
|
||||
void test_router_cluster_telemetry()
|
||||
{
|
||||
TEST_MESSAGE("=== Router cluster: telemetry broadcast ===");
|
||||
TEST_MESSAGE("Topology: 71 nodes with a concentrated 45-node cluster at hop 2.");
|
||||
TEST_MESSAGE("Expectation: result stays in the 2-4 range.");
|
||||
|
||||
auto shim = std::unique_ptr<HopScalingTestShim>(new HopScalingTestShim());
|
||||
hopScalingModule = shim.get();
|
||||
buildRouterCluster();
|
||||
const uint16_t distD[HOP_MAX + 1] = {3, 5, 45, 8, 3, 2, 2, 3};
|
||||
injectSampleTraffic(*shim, 0x94000000, distD);
|
||||
shim->runOnce();
|
||||
|
||||
TEST_MSG_FMT("Router cluster: hop=%u", shim->getLastRequiredHop());
|
||||
TEST_ASSERT_TRUE(shim->getLastRequiredHop() >= 2);
|
||||
TEST_ASSERT_TRUE(shim->getLastRequiredHop() <= 4);
|
||||
assertCompactHistogramActive(*shim);
|
||||
|
||||
hopScalingModule = nullptr;
|
||||
}
|
||||
|
||||
void test_megamesh_eviction_scaling()
|
||||
{
|
||||
TEST_MESSAGE("=== Megamesh with eviction scaling ===");
|
||||
TEST_MESSAGE("Topology: NodeDB at capacity (199 nodes), ~2000-node mesh with sustained eviction pressure.");
|
||||
TEST_MESSAGE("Expectation: sustained evictions tracked in rolling average, hop stays well below HOP_MAX.");
|
||||
|
||||
auto shim = std::unique_ptr<HopScalingTestShim>(new HopScalingTestShim());
|
||||
hopScalingModule = shim.get();
|
||||
buildMegamesh();
|
||||
|
||||
const uint16_t distE[HOP_MAX + 1] = {301, 402, 352, 301, 201, 151, 141, 151};
|
||||
injectSampleTraffic(*shim, 0x9B000000, distE);
|
||||
|
||||
shim->runOnce();
|
||||
uint8_t hopBefore = shim->getLastRequiredHop();
|
||||
TEST_MSG_FMT("Megamesh initial: hop=%u", hopBefore);
|
||||
|
||||
for (int hour = 0; hour < 3; hour++) {
|
||||
mockTime += ONE_HOUR_MS;
|
||||
{
|
||||
const uint16_t megaDist[HOP_MAX + 1] = {301, 402, 352, 301, 201, 151, 141, 151};
|
||||
uint16_t ordinal = 0;
|
||||
for (uint8_t hop = 0; hop <= HOP_MAX; ++hop) {
|
||||
for (uint16_t n = 0; n < megaDist[hop]; ++n) {
|
||||
const uint32_t nodeId = makeDistributedNodeId(0x9C000000u, ordinal, static_cast<uint32_t>(hour) * 0x10000u);
|
||||
shim->samplePacketForHistogram(nodeId, hop);
|
||||
++ordinal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int run = 0; run < 7; run++)
|
||||
shim->runOnce();
|
||||
|
||||
TEST_MSG_FMT("Megamesh hour %d: hop=%u", hour + 1, shim->getLastRequiredHop());
|
||||
}
|
||||
|
||||
TEST_MESSAGE("Assertion: hop stays well below HOP_MAX on a large-distribution mesh.");
|
||||
TEST_ASSERT_TRUE(shim->getLastRequiredHop() <= 3);
|
||||
assertCompactHistogramActive(*shim);
|
||||
|
||||
hopScalingModule = nullptr;
|
||||
}
|
||||
|
||||
void test_sparse_to_dense_transition()
|
||||
{
|
||||
TEST_MESSAGE("=== Sparse-to-dense transition ===");
|
||||
TEST_MESSAGE("Topology change: start with a 22-node deep chain, then inject 50 new neighbors at hops 0-1.");
|
||||
TEST_MESSAGE("Expectation: hop drops sharply once the local neighborhood becomes dense.");
|
||||
|
||||
auto shim = std::unique_ptr<HopScalingTestShim>(new HopScalingTestShim());
|
||||
hopScalingModule = shim.get();
|
||||
|
||||
buildDeepLinearChain();
|
||||
const uint16_t distC2[HOP_MAX + 1] = {2, 3, 3, 4, 3, 2, 2, 3};
|
||||
injectSampleTraffic(*shim, 0x95000000, distC2);
|
||||
shim->runOnce();
|
||||
uint8_t hopSparse = shim->getLastRequiredHop();
|
||||
TEST_MSG_FMT("Phase 1 sparse: hop=%u (expect %u)", hopSparse, HOP_MAX);
|
||||
TEST_ASSERT_EQUAL_UINT8(HOP_MAX, hopSparse);
|
||||
|
||||
addNodesAtHop(0xA000, 0, 25, 120);
|
||||
addNodesAtHop(0xB000, 1, 25, 300);
|
||||
|
||||
for (uint32_t i = 0; i < 25; ++i)
|
||||
shim->samplePacketForHistogram(makeDistributedNodeId(0xA000, i, static_cast<uint32_t>(0) << 8), 0);
|
||||
for (uint32_t i = 0; i < 25; ++i)
|
||||
shim->samplePacketForHistogram(makeDistributedNodeId(0xB000, i, static_cast<uint32_t>(1) << 8), 1);
|
||||
|
||||
for (int run = 0; run < HopScalingModule::RUNS_PER_HOUR; run++)
|
||||
shim->runOnce();
|
||||
|
||||
uint8_t hopDense = shim->getLastRequiredHop();
|
||||
TEST_MSG_FMT("Phase 2 dense: hop=%u (expect <= 3)", hopDense);
|
||||
|
||||
TEST_ASSERT_TRUE(hopDense < hopSparse);
|
||||
TEST_ASSERT_TRUE(hopDense <= 3);
|
||||
assertCompactHistogramActive(*shim);
|
||||
|
||||
hopScalingModule = nullptr;
|
||||
}
|
||||
|
||||
void test_state_persistence()
|
||||
{
|
||||
TEST_MESSAGE("=== State persistence across restart ===");
|
||||
TEST_MESSAGE("Expectation: histogram entries survive instance teardown and reload.");
|
||||
|
||||
{
|
||||
auto shim = std::unique_ptr<HopScalingTestShim>(new HopScalingTestShim());
|
||||
hopScalingModule = shim.get();
|
||||
|
||||
const uint16_t dist[HOP_MAX + 1] = {5, 8, 12, 10, 5, 3, 2, 1};
|
||||
injectSampleTraffic(*shim, 0x9D000000, dist, 2);
|
||||
|
||||
TEST_MSG_FMT("Phase 1: entries=%u hop=%u", shim->getEntryCount(), shim->getLastRequiredHop());
|
||||
TEST_ASSERT_GREATER_THAN_UINT8(0, shim->getEntryCount());
|
||||
hopScalingModule = nullptr;
|
||||
}
|
||||
|
||||
{
|
||||
auto shim = std::unique_ptr<HopScalingTestShim>(new HopScalingTestShim());
|
||||
hopScalingModule = shim.get();
|
||||
shim->runOnce();
|
||||
|
||||
TEST_MSG_FMT("Phase 2 restored: entries=%u hop=%u", shim->getEntryCount(), shim->getLastRequiredHop());
|
||||
TEST_ASSERT_GREATER_THAN_UINT8(0, shim->getEntryCount());
|
||||
hopScalingModule = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void test_hourly_roll()
|
||||
{
|
||||
TEST_MESSAGE("=== Hourly roll cycle ===");
|
||||
TEST_MESSAGE("Expectation: histogram accumulates data and provides valid hop recommendation after multiple rolls.");
|
||||
|
||||
auto shim = std::unique_ptr<HopScalingTestShim>(new HopScalingTestShim());
|
||||
hopScalingModule = shim.get();
|
||||
buildSpreadSparseMesh();
|
||||
shim->setHistogramDenominator(HopScalingModule::DENOM_MIN);
|
||||
|
||||
for (uint32_t i = 1; i <= 30; i++) {
|
||||
const uint32_t nodeId = makeDistributedNodeId(0x97000000, i, 0xAAu);
|
||||
shim->samplePacketForHistogram(nodeId, static_cast<uint8_t>(i % (HOP_MAX + 1)));
|
||||
}
|
||||
|
||||
for (int run = 0; run < 13; run++) {
|
||||
int32_t interval = shim->runOnce();
|
||||
TEST_ASSERT_GREATER_THAN(0, interval);
|
||||
}
|
||||
|
||||
TEST_MSG_FMT("Hourly roll: hop=%u entries=%u", shim->getLastRequiredHop(), shim->getEntryCount());
|
||||
assertCompactHistogramActive(*shim);
|
||||
|
||||
hopScalingModule = nullptr;
|
||||
}
|
||||
|
||||
void test_intermediate_status()
|
||||
{
|
||||
TEST_MESSAGE("=== Intermediate status (no recomputation) ===");
|
||||
TEST_MESSAGE("Expectation: runs between hourly updates leave hop unchanged.");
|
||||
|
||||
auto shim = std::unique_ptr<HopScalingTestShim>(new HopScalingTestShim());
|
||||
hopScalingModule = shim.get();
|
||||
buildRouterCluster();
|
||||
const uint16_t distD[HOP_MAX + 1] = {3, 5, 45, 8, 3, 2, 2, 3};
|
||||
injectSampleTraffic(*shim, 0x98000000, distD);
|
||||
|
||||
shim->runOnce();
|
||||
uint8_t hopAfterInitial = shim->getLastRequiredHop();
|
||||
TEST_MSG_FMT("Initial: hop=%u", hopAfterInitial);
|
||||
|
||||
for (int run = 0; run < 3; run++) {
|
||||
shim->runOnce();
|
||||
TEST_ASSERT_EQUAL_UINT8(hopAfterInitial, shim->getLastRequiredHop());
|
||||
}
|
||||
|
||||
TEST_MSG_FMT("After 3 intermediate runs: hop=%u (unchanged)", shim->getLastRequiredHop());
|
||||
hopScalingModule = nullptr;
|
||||
}
|
||||
|
||||
void test_startup_blank_state()
|
||||
{
|
||||
TEST_MESSAGE("=== Startup with blank state ===");
|
||||
TEST_MESSAGE("Expectation: fresh instance starts with zeroed rolling averages and a valid hop result.");
|
||||
|
||||
#ifdef FSCom
|
||||
FSCom.remove("/prefs/hopScalingState.bin");
|
||||
#endif
|
||||
|
||||
auto shim = std::unique_ptr<HopScalingTestShim>(new HopScalingTestShim());
|
||||
hopScalingModule = shim.get();
|
||||
buildDeepLinearChain();
|
||||
|
||||
int32_t interval = shim->runOnce();
|
||||
|
||||
TEST_ASSERT_GREATER_THAN(0, interval);
|
||||
TEST_ASSERT_TRUE(shim->getLastRequiredHop() <= HOP_MAX);
|
||||
|
||||
TEST_MSG_FMT("Startup blank: hop=%u", shim->getLastRequiredHop());
|
||||
|
||||
hopScalingModule = nullptr;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests — Denominator state machine
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void test_denominator_rises_on_overflow()
|
||||
{
|
||||
TEST_MESSAGE("=== samplingDenominator doubles when histogram overflows ===");
|
||||
TEST_MESSAGE("Fill to > FILL_HIGH_PCT with forceInsertEntry, then trigger via samplePacketForHistogram.");
|
||||
TEST_MESSAGE("Expectation: samp/filt both double to 2, hold set to FILTER_DENOM_HOLD_ROLLS.");
|
||||
|
||||
auto shim = std::unique_ptr<HopScalingTestShim>(new HopScalingTestShim());
|
||||
hopScalingModule = shim.get();
|
||||
|
||||
// Insert 103 entries with hashes 1..103 (all distinct, no sampling-filter skew).
|
||||
// 103 / 128 = 80.4% fill, which meets FILL_HIGH_PCT=80.
|
||||
// Odd hashes (1,3,...,103) will be evicted when denom doubles to 2; even ones survive.
|
||||
static constexpr uint8_t FILL_COUNT = 103u;
|
||||
for (uint8_t i = 1; i <= FILL_COUNT; i++)
|
||||
shim->forceInsertEntry(i, 2u);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT8(HopScalingModule::DENOM_MIN, shim->getSamplingDenominator());
|
||||
TEST_ASSERT_EQUAL_UINT8(HopScalingModule::DENOM_MIN, shim->getFilteringDenominator());
|
||||
TEST_ASSERT_EQUAL_UINT8(0u, shim->getFilteringDenomHoldRollsRemaining());
|
||||
TEST_ASSERT_EQUAL_UINT8(FILL_COUNT, shim->getEntryCount());
|
||||
|
||||
// A new node passes the denom=1 admission gate; fill ≥ 80% triggers trimIfNeeded → doubling.
|
||||
shim->samplePacketForHistogram(0xB0000000u, 1u);
|
||||
|
||||
TEST_MSG_FMT("After scale-up: samp=1/%u filt=1/%u holdRolls=%u entries=%u", shim->getSamplingDenominator(),
|
||||
shim->getFilteringDenominator(), shim->getFilteringDenomHoldRollsRemaining(), shim->getEntryCount());
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT8(2u, shim->getSamplingDenominator());
|
||||
TEST_ASSERT_EQUAL_UINT8(2u, shim->getFilteringDenominator());
|
||||
TEST_ASSERT_EQUAL_UINT8(HopScalingModule::FILTER_DENOM_HOLD_ROLLS, shim->getFilteringDenomHoldRollsRemaining());
|
||||
// After evicting entries with (hash & 1) != 0, roughly half the entries remain.
|
||||
TEST_ASSERT_LESS_THAN_UINT8(FILL_COUNT, shim->getEntryCount());
|
||||
|
||||
hopScalingModule = nullptr;
|
||||
}
|
||||
|
||||
void test_filtering_denom_hold_counts_down()
|
||||
{
|
||||
TEST_MESSAGE("=== filteringDenominator held while hold counter > 0 ===");
|
||||
TEST_MESSAGE("Force filt=4 samp=1 hold=3; verify no step for 2 rolls, then step fires on roll 3.");
|
||||
|
||||
auto shim = std::unique_ptr<HopScalingTestShim>(new HopScalingTestShim());
|
||||
hopScalingModule = shim.get();
|
||||
|
||||
// samp=DENOM_MIN so scale-down in step 4 can't go lower; hold=3 for a short, fast test.
|
||||
shim->forceFilterDenomState(HopScalingModule::DENOM_MIN, 4u, 3u);
|
||||
|
||||
shim->rollHourTest(); // hold 3→2, no step
|
||||
TEST_ASSERT_EQUAL_UINT8(4u, shim->getFilteringDenominator());
|
||||
TEST_ASSERT_EQUAL_UINT8(2u, shim->getFilteringDenomHoldRollsRemaining());
|
||||
|
||||
shim->rollHourTest(); // hold 2→1, no step
|
||||
TEST_ASSERT_EQUAL_UINT8(4u, shim->getFilteringDenominator());
|
||||
TEST_ASSERT_EQUAL_UINT8(1u, shim->getFilteringDenomHoldRollsRemaining());
|
||||
|
||||
// Roll 3: hold 1→0, step fires — filteringDenominator halves to max(2, samp=1) = 2.
|
||||
shim->rollHourTest();
|
||||
TEST_MSG_FMT("After hold expires: filt=1/%u samp=1/%u holdRolls=%u", shim->getFilteringDenominator(),
|
||||
shim->getSamplingDenominator(), shim->getFilteringDenomHoldRollsRemaining());
|
||||
TEST_ASSERT_EQUAL_UINT8(2u, shim->getFilteringDenominator());
|
||||
TEST_ASSERT_EQUAL_UINT8(0u, shim->getFilteringDenomHoldRollsRemaining());
|
||||
|
||||
hopScalingModule = nullptr;
|
||||
}
|
||||
|
||||
void test_filtering_denom_steps_down_gradually()
|
||||
{
|
||||
TEST_MESSAGE("=== filteringDenominator descends one halving per rollHour() after hold expires ===");
|
||||
TEST_MESSAGE("Force filt=8 samp=1 hold=1; expect 8→4→2→1 over 3 rolls, then stable.");
|
||||
|
||||
auto shim = std::unique_ptr<HopScalingTestShim>(new HopScalingTestShim());
|
||||
hopScalingModule = shim.get();
|
||||
|
||||
shim->forceFilterDenomState(HopScalingModule::DENOM_MIN, 8u, 1u);
|
||||
|
||||
shim->rollHourTest(); // hold 1→0, step: 8/2=4 > 1, filt=4
|
||||
TEST_ASSERT_EQUAL_UINT8(4u, shim->getFilteringDenominator());
|
||||
|
||||
shim->rollHourTest(); // hold=0 (no decrement), step: 4/2=2 > 1, filt=2
|
||||
TEST_ASSERT_EQUAL_UINT8(2u, shim->getFilteringDenominator());
|
||||
|
||||
shim->rollHourTest(); // step: 2/2=1, not > samp=1, filt=samp=1 — converged
|
||||
TEST_ASSERT_EQUAL_UINT8(1u, shim->getFilteringDenominator());
|
||||
|
||||
shim->rollHourTest(); // filt==samp, outer if is false — no further change
|
||||
TEST_ASSERT_EQUAL_UINT8(1u, shim->getFilteringDenominator());
|
||||
TEST_ASSERT_EQUAL_UINT8(HopScalingModule::DENOM_MIN, shim->getSamplingDenominator());
|
||||
|
||||
hopScalingModule = nullptr;
|
||||
}
|
||||
|
||||
void test_full_at_denom_max_drops_entry()
|
||||
{
|
||||
TEST_MESSAGE("=== Full histogram at DENOM_MAX drops new entries ===");
|
||||
TEST_MESSAGE("Fill CAPACITY entries, force samp=DENOM_MAX, sample admissible node.");
|
||||
TEST_MESSAGE("Expectation: entry count stays at CAPACITY (LOG_WARN fires; visible in test output).");
|
||||
|
||||
auto shim = std::unique_ptr<HopScalingTestShim>(new HopScalingTestShim());
|
||||
hopScalingModule = shim.get();
|
||||
shim->setHashSeed(0); // deterministic hash for admissible-ID search
|
||||
|
||||
shim->forceFilterDenomState(HopScalingModule::DENOM_MAX, HopScalingModule::DENOM_MAX, 0u);
|
||||
|
||||
// Fill with odd hashes 1,3,5,...,(2*CAPACITY-1). None are multiples of 128, so none
|
||||
// collide with the admissible node's hash (which must be a multiple of 128).
|
||||
for (uint16_t i = 0; i < HopScalingModule::CAPACITY; i++)
|
||||
shim->forceInsertEntry(static_cast<uint16_t>(2u * i + 1u), 1u);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT8(HopScalingModule::CAPACITY, shim->getEntryCount());
|
||||
|
||||
// Find a node ID whose hash passes DENOM_MAX, i.e. (hash & 127) == 0.
|
||||
uint32_t admissibleId = 0;
|
||||
for (uint32_t id = 1u; id < 0x10000u; id++) {
|
||||
if ((shim->hashNodeIdPublic(id) & (HopScalingModule::DENOM_MAX - 1u)) == 0u) {
|
||||
admissibleId = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
TEST_ASSERT_NOT_EQUAL(0u, admissibleId); // sanity: the hash space is dense enough to find one quickly
|
||||
|
||||
shim->samplePacketForHistogram(admissibleId, 3u);
|
||||
|
||||
TEST_MSG_FMT("After drop attempt: entries=%u CAPACITY=%u admissibleId=0x%08x hash=0x%04x", shim->getEntryCount(),
|
||||
static_cast<unsigned>(HopScalingModule::CAPACITY), admissibleId,
|
||||
static_cast<unsigned>(shim->hashNodeIdPublic(admissibleId)));
|
||||
TEST_ASSERT_EQUAL_UINT8(HopScalingModule::CAPACITY, shim->getEntryCount());
|
||||
|
||||
hopScalingModule = nullptr;
|
||||
}
|
||||
|
||||
void test_scenario_summary_output()
|
||||
{
|
||||
TEST_MESSAGE("=== Scenario summary ===");
|
||||
TEST_MESSAGE("Scenario | Nodes | Distribution | Hop | Why");
|
||||
TEST_MESSAGE("A: Dense local | 110 | 25/30/15/5/10/15/10 h0-6 | 1-2 | 55 nodes at h1 >> 40");
|
||||
TEST_MESSAGE("B: Spread | 76 | 5/8/12/15/10/6/10/10 h0-7 | 3-4 | Need h3 to reach 40");
|
||||
TEST_MESSAGE("C: Deep chain | 22 | 2/3/3/4/3/2/2/3 h0-7 | 7 | Never reaches 40");
|
||||
TEST_MESSAGE("D: Router | 71 | 3/5/45/8/3/2/2/3 h0-7 | 2-3 | 45-node hop-2 cluster");
|
||||
TEST_MESSAGE("E: Megamesh | 199 | 30/40/35/30/20/15/14/15 h0-7 | 0-1 | Dense low-hop histogram");
|
||||
TEST_MESSAGE("F: Transition | 22->72 | Chain -> dense local | 7-><=3 | Adapts to new neighbors");
|
||||
TEST_MESSAGE("G: Persistence | -- | -- | -- | Eviction avg survives reboot");
|
||||
TEST_MESSAGE("");
|
||||
TEST_MESSAGE("=== Denominator state machine summary ===");
|
||||
TEST_MESSAGE("Test | Pre-condition | Expectation");
|
||||
TEST_MESSAGE("H: Rises on overflow | 103 entries forced, denom=1 | samp/filt→2, holdRolls=13");
|
||||
TEST_MESSAGE(
|
||||
"I: Hold counts down | filt=4 samp=1 hold=3 | no step for 2 rolls, step on roll 3: filt→2");
|
||||
TEST_MESSAGE("J: Steps down gradually | filt=8 samp=1 hold=1 | 8→4→2→1 over 3 rolls, stable on 4th");
|
||||
TEST_MESSAGE("K: Full at DENOM_MAX drops entry | 128 entries, samp=filt=128 | count stays 128, LOG_WARN emitted");
|
||||
}
|
||||
|
||||
static void test_memory_layout()
|
||||
{
|
||||
TEST_MSG_FMT("%-35s %6s %s", "Type", "bytes", "Notes");
|
||||
TEST_MSG_FMT("%-35s %6zu %s", "Record", sizeof(Record), "nodeHash:16 + hops:3 + seen:13 (32-bit packed)");
|
||||
TEST_MSG_FMT("%-35s %6zu %s", "HopScalingModule::PerHopCounts", sizeof(HopScalingModule::PerHopCounts),
|
||||
"perHop[8](16) + total(2)");
|
||||
TEST_MSG_FMT("%-35s %6zu %s", "HopScalingModule (instance)", HopScalingTestShim::sizeofSelf(),
|
||||
"entries[128](512) + denom state + cached results + OSThread overhead");
|
||||
|
||||
TEST_PASS();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Unity setup / teardown / main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void setUp(void)
|
||||
{
|
||||
if (!mockNodeDB)
|
||||
mockNodeDB = new MockNodeDB();
|
||||
mockNodeDB->clearTestNodes();
|
||||
|
||||
config = meshtastic_LocalConfig_init_zero;
|
||||
moduleConfig = meshtastic_LocalModuleConfig_init_zero;
|
||||
myNodeInfo.my_node_num = kLocalNode;
|
||||
nodeDB = mockNodeDB;
|
||||
|
||||
#ifdef FSCom
|
||||
FSCom.remove("/prefs/hopScalingState.bin");
|
||||
#endif
|
||||
|
||||
// Reset mock clock to a known base (1 hour in so subtraction never underflows)
|
||||
mockTime = ONE_HOUR_MS;
|
||||
}
|
||||
|
||||
void tearDown(void)
|
||||
{
|
||||
hopScalingModule = nullptr;
|
||||
}
|
||||
|
||||
void setup()
|
||||
{
|
||||
initializeTestEnvironment();
|
||||
nodeDB = mockNodeDB;
|
||||
|
||||
UNITY_BEGIN();
|
||||
|
||||
printf("\n=== Topology-driven hop reduction ===\n");
|
||||
RUN_TEST(test_dense_local_telemetry);
|
||||
RUN_TEST(test_spread_sparse_position);
|
||||
RUN_TEST(test_deep_chain_position);
|
||||
RUN_TEST(test_router_cluster_telemetry);
|
||||
RUN_TEST(test_megamesh_eviction_scaling);
|
||||
RUN_TEST(test_sparse_to_dense_transition);
|
||||
|
||||
printf("\n=== Lifecycle ===\n");
|
||||
RUN_TEST(test_state_persistence);
|
||||
RUN_TEST(test_hourly_roll);
|
||||
RUN_TEST(test_intermediate_status);
|
||||
RUN_TEST(test_startup_blank_state);
|
||||
|
||||
printf("\n=== Denominator state machine ===\n");
|
||||
RUN_TEST(test_denominator_rises_on_overflow);
|
||||
RUN_TEST(test_filtering_denom_hold_counts_down);
|
||||
RUN_TEST(test_filtering_denom_steps_down_gradually);
|
||||
RUN_TEST(test_full_at_denom_max_drops_entry);
|
||||
|
||||
printf("\n=== Summary ===\n");
|
||||
RUN_TEST(test_memory_layout);
|
||||
RUN_TEST(test_scenario_summary_output);
|
||||
|
||||
exit(UNITY_END());
|
||||
}
|
||||
|
||||
void loop() {}
|
||||
|
||||
#else // !HAS_VARIABLE_HOPS
|
||||
|
||||
void setUp(void) {}
|
||||
void tearDown(void) {}
|
||||
|
||||
void setup()
|
||||
{
|
||||
initializeTestEnvironment();
|
||||
UNITY_BEGIN();
|
||||
exit(UNITY_END());
|
||||
}
|
||||
|
||||
void loop() {}
|
||||
|
||||
#endif
|
||||
@@ -1,42 +1,31 @@
|
||||
#include "../test_helpers.h"
|
||||
|
||||
// Helper function for all encrypted packet assertions
|
||||
void assert_encrypted_packet(const std::string &json, meshtastic_MeshPacket packet)
|
||||
static void assert_encrypted_packet(const std::string &json, const meshtastic_MeshPacket &packet)
|
||||
{
|
||||
// Parse and validate JSON
|
||||
TEST_ASSERT_TRUE(json.length() > 0);
|
||||
|
||||
JSONValue *root = JSON::Parse(json.c_str());
|
||||
TEST_ASSERT_NOT_NULL(root);
|
||||
TEST_ASSERT_TRUE(root->IsObject());
|
||||
Json::Value root = parse_json(json);
|
||||
TEST_ASSERT_TRUE(root.isObject());
|
||||
|
||||
JSONObject jsonObj = root->AsObject();
|
||||
TEST_ASSERT_TRUE(root.isMember("from"));
|
||||
TEST_ASSERT_EQUAL(packet.from, root["from"].asUInt());
|
||||
|
||||
// Assert basic packet fields
|
||||
TEST_ASSERT_TRUE(jsonObj.find("from") != jsonObj.end());
|
||||
TEST_ASSERT_EQUAL(packet.from, (uint32_t)jsonObj.at("from")->AsNumber());
|
||||
TEST_ASSERT_TRUE(root.isMember("to"));
|
||||
TEST_ASSERT_EQUAL(packet.to, root["to"].asUInt());
|
||||
|
||||
TEST_ASSERT_TRUE(jsonObj.find("to") != jsonObj.end());
|
||||
TEST_ASSERT_EQUAL(packet.to, (uint32_t)jsonObj.at("to")->AsNumber());
|
||||
TEST_ASSERT_TRUE(root.isMember("id"));
|
||||
TEST_ASSERT_EQUAL(packet.id, root["id"].asUInt());
|
||||
|
||||
TEST_ASSERT_TRUE(jsonObj.find("id") != jsonObj.end());
|
||||
TEST_ASSERT_EQUAL(packet.id, (uint32_t)jsonObj.at("id")->AsNumber());
|
||||
TEST_ASSERT_TRUE(root.isMember("bytes"));
|
||||
TEST_ASSERT_TRUE(root["bytes"].isString());
|
||||
|
||||
// Assert encrypted data fields
|
||||
TEST_ASSERT_TRUE(jsonObj.find("bytes") != jsonObj.end());
|
||||
TEST_ASSERT_TRUE(jsonObj.at("bytes")->IsString());
|
||||
TEST_ASSERT_TRUE(root.isMember("size"));
|
||||
TEST_ASSERT_EQUAL(packet.encrypted.size, (int)root["size"].asInt());
|
||||
|
||||
TEST_ASSERT_TRUE(jsonObj.find("size") != jsonObj.end());
|
||||
TEST_ASSERT_EQUAL(packet.encrypted.size, (int)jsonObj.at("size")->AsNumber());
|
||||
|
||||
// Assert hex encoding
|
||||
std::string encrypted_hex = jsonObj["bytes"]->AsString();
|
||||
std::string encrypted_hex = root["bytes"].asString();
|
||||
TEST_ASSERT_EQUAL(packet.encrypted.size * 2, encrypted_hex.length());
|
||||
|
||||
delete root;
|
||||
}
|
||||
|
||||
// Test encrypted packet serialization
|
||||
void test_encrypted_packet_serialization()
|
||||
{
|
||||
const char *data = "encrypted_payload_data";
|
||||
@@ -48,7 +37,6 @@ void test_encrypted_packet_serialization()
|
||||
assert_encrypted_packet(json, packet);
|
||||
}
|
||||
|
||||
// Test empty encrypted packet
|
||||
void test_empty_encrypted_packet()
|
||||
{
|
||||
meshtastic_MeshPacket packet =
|
||||
|
||||
@@ -13,7 +13,6 @@ static size_t encode_user_info(uint8_t *buffer, size_t buffer_size)
|
||||
return stream.bytes_written;
|
||||
}
|
||||
|
||||
// Test NODEINFO_APP port
|
||||
void test_nodeinfo_serialization()
|
||||
{
|
||||
uint8_t buffer[256];
|
||||
@@ -24,28 +23,20 @@ void test_nodeinfo_serialization()
|
||||
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
|
||||
TEST_ASSERT_TRUE(json.length() > 0);
|
||||
|
||||
JSONValue *root = JSON::Parse(json.c_str());
|
||||
TEST_ASSERT_NOT_NULL(root);
|
||||
TEST_ASSERT_TRUE(root->IsObject());
|
||||
Json::Value root = parse_json(json);
|
||||
TEST_ASSERT_TRUE(root.isObject());
|
||||
|
||||
JSONObject jsonObj = root->AsObject();
|
||||
TEST_ASSERT_TRUE(root.isMember("type"));
|
||||
TEST_ASSERT_EQUAL_STRING("nodeinfo", root["type"].asString().c_str());
|
||||
|
||||
// Check message type
|
||||
TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end());
|
||||
TEST_ASSERT_EQUAL_STRING("nodeinfo", jsonObj["type"]->AsString().c_str());
|
||||
TEST_ASSERT_TRUE(root.isMember("payload"));
|
||||
TEST_ASSERT_TRUE(root["payload"].isObject());
|
||||
|
||||
// Check payload
|
||||
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
|
||||
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
|
||||
const Json::Value &payload = root["payload"];
|
||||
|
||||
JSONObject payload = jsonObj["payload"]->AsObject();
|
||||
TEST_ASSERT_TRUE(payload.isMember("shortname"));
|
||||
TEST_ASSERT_EQUAL_STRING("TEST", payload["shortname"].asString().c_str());
|
||||
|
||||
// Verify user data
|
||||
TEST_ASSERT_TRUE(payload.find("shortname") != payload.end());
|
||||
TEST_ASSERT_EQUAL_STRING("TEST", payload["shortname"]->AsString().c_str());
|
||||
|
||||
TEST_ASSERT_TRUE(payload.find("longname") != payload.end());
|
||||
TEST_ASSERT_EQUAL_STRING("Test User", payload["longname"]->AsString().c_str());
|
||||
|
||||
delete root;
|
||||
TEST_ASSERT_TRUE(payload.isMember("longname"));
|
||||
TEST_ASSERT_EQUAL_STRING("Test User", payload["longname"].asString().c_str());
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
static size_t encode_position(uint8_t *buffer, size_t buffer_size)
|
||||
{
|
||||
meshtastic_Position position = meshtastic_Position_init_zero;
|
||||
position.latitude_i = 374208000; // 37.4208 degrees * 1e7
|
||||
position.longitude_i = -1221981000; // -122.1981 degrees * 1e7
|
||||
position.latitude_i = 374208000;
|
||||
position.longitude_i = -1221981000;
|
||||
position.altitude = 123;
|
||||
position.time = 1609459200;
|
||||
position.has_altitude = true;
|
||||
@@ -16,7 +16,6 @@ static size_t encode_position(uint8_t *buffer, size_t buffer_size)
|
||||
return stream.bytes_written;
|
||||
}
|
||||
|
||||
// Test POSITION_APP port
|
||||
void test_position_serialization()
|
||||
{
|
||||
uint8_t buffer[256];
|
||||
@@ -27,31 +26,23 @@ void test_position_serialization()
|
||||
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
|
||||
TEST_ASSERT_TRUE(json.length() > 0);
|
||||
|
||||
JSONValue *root = JSON::Parse(json.c_str());
|
||||
TEST_ASSERT_NOT_NULL(root);
|
||||
TEST_ASSERT_TRUE(root->IsObject());
|
||||
Json::Value root = parse_json(json);
|
||||
TEST_ASSERT_TRUE(root.isObject());
|
||||
|
||||
JSONObject jsonObj = root->AsObject();
|
||||
TEST_ASSERT_TRUE(root.isMember("type"));
|
||||
TEST_ASSERT_EQUAL_STRING("position", root["type"].asString().c_str());
|
||||
|
||||
// Check message type
|
||||
TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end());
|
||||
TEST_ASSERT_EQUAL_STRING("position", jsonObj["type"]->AsString().c_str());
|
||||
TEST_ASSERT_TRUE(root.isMember("payload"));
|
||||
TEST_ASSERT_TRUE(root["payload"].isObject());
|
||||
|
||||
// Check payload
|
||||
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
|
||||
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
|
||||
const Json::Value &payload = root["payload"];
|
||||
|
||||
JSONObject payload = jsonObj["payload"]->AsObject();
|
||||
TEST_ASSERT_TRUE(payload.isMember("latitude_i"));
|
||||
TEST_ASSERT_EQUAL(374208000, payload["latitude_i"].asInt());
|
||||
|
||||
// Verify position data
|
||||
TEST_ASSERT_TRUE(payload.find("latitude_i") != payload.end());
|
||||
TEST_ASSERT_EQUAL(374208000, (int)payload["latitude_i"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.isMember("longitude_i"));
|
||||
TEST_ASSERT_EQUAL(-1221981000, payload["longitude_i"].asInt());
|
||||
|
||||
TEST_ASSERT_TRUE(payload.find("longitude_i") != payload.end());
|
||||
TEST_ASSERT_EQUAL(-1221981000, (int)payload["longitude_i"]->AsNumber());
|
||||
|
||||
TEST_ASSERT_TRUE(payload.find("altitude") != payload.end());
|
||||
TEST_ASSERT_EQUAL(123, (int)payload["altitude"]->AsNumber());
|
||||
|
||||
delete root;
|
||||
TEST_ASSERT_TRUE(payload.isMember("altitude"));
|
||||
TEST_ASSERT_EQUAL(123, payload["altitude"].asInt());
|
||||
}
|
||||
|
||||
@@ -1,6 +1,62 @@
|
||||
#include "../test_helpers.h"
|
||||
|
||||
// Helper function to create and encode device metrics
|
||||
static void fill_all_env_metrics(meshtastic_Telemetry &telemetry)
|
||||
{
|
||||
telemetry.variant.environment_metrics.temperature = 23.56f;
|
||||
telemetry.variant.environment_metrics.has_temperature = true;
|
||||
telemetry.variant.environment_metrics.relative_humidity = 65.43f;
|
||||
telemetry.variant.environment_metrics.has_relative_humidity = true;
|
||||
telemetry.variant.environment_metrics.barometric_pressure = 1013.27f;
|
||||
telemetry.variant.environment_metrics.has_barometric_pressure = true;
|
||||
|
||||
telemetry.variant.environment_metrics.gas_resistance = 50.58f;
|
||||
telemetry.variant.environment_metrics.has_gas_resistance = true;
|
||||
telemetry.variant.environment_metrics.iaq = 120;
|
||||
telemetry.variant.environment_metrics.has_iaq = true;
|
||||
|
||||
telemetry.variant.environment_metrics.voltage = 3.34f;
|
||||
telemetry.variant.environment_metrics.has_voltage = true;
|
||||
telemetry.variant.environment_metrics.current = 0.53f;
|
||||
telemetry.variant.environment_metrics.has_current = true;
|
||||
|
||||
telemetry.variant.environment_metrics.lux = 450.12f;
|
||||
telemetry.variant.environment_metrics.has_lux = true;
|
||||
telemetry.variant.environment_metrics.white_lux = 380.95f;
|
||||
telemetry.variant.environment_metrics.has_white_lux = true;
|
||||
telemetry.variant.environment_metrics.ir_lux = 25.37f;
|
||||
telemetry.variant.environment_metrics.has_ir_lux = true;
|
||||
telemetry.variant.environment_metrics.uv_lux = 15.68f;
|
||||
telemetry.variant.environment_metrics.has_uv_lux = true;
|
||||
|
||||
telemetry.variant.environment_metrics.distance = 150.29f;
|
||||
telemetry.variant.environment_metrics.has_distance = true;
|
||||
|
||||
telemetry.variant.environment_metrics.wind_direction = 180;
|
||||
telemetry.variant.environment_metrics.has_wind_direction = true;
|
||||
telemetry.variant.environment_metrics.wind_speed = 5.52f;
|
||||
telemetry.variant.environment_metrics.has_wind_speed = true;
|
||||
telemetry.variant.environment_metrics.wind_gust = 8.24f;
|
||||
telemetry.variant.environment_metrics.has_wind_gust = true;
|
||||
telemetry.variant.environment_metrics.wind_lull = 2.13f;
|
||||
telemetry.variant.environment_metrics.has_wind_lull = true;
|
||||
|
||||
telemetry.variant.environment_metrics.weight = 75.56f;
|
||||
telemetry.variant.environment_metrics.has_weight = true;
|
||||
|
||||
telemetry.variant.environment_metrics.radiation = 0.13f;
|
||||
telemetry.variant.environment_metrics.has_radiation = true;
|
||||
|
||||
telemetry.variant.environment_metrics.rainfall_1h = 2.57f;
|
||||
telemetry.variant.environment_metrics.has_rainfall_1h = true;
|
||||
telemetry.variant.environment_metrics.rainfall_24h = 15.89f;
|
||||
telemetry.variant.environment_metrics.has_rainfall_24h = true;
|
||||
|
||||
telemetry.variant.environment_metrics.soil_moisture = 85;
|
||||
telemetry.variant.environment_metrics.has_soil_moisture = true;
|
||||
telemetry.variant.environment_metrics.soil_temperature = 18.54f;
|
||||
telemetry.variant.environment_metrics.has_soil_temperature = true;
|
||||
}
|
||||
|
||||
static size_t encode_telemetry_device_metrics(uint8_t *buffer, size_t buffer_size)
|
||||
{
|
||||
meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero;
|
||||
@@ -22,507 +78,233 @@ static size_t encode_telemetry_device_metrics(uint8_t *buffer, size_t buffer_siz
|
||||
return stream.bytes_written;
|
||||
}
|
||||
|
||||
// Helper function to create and encode empty environment metrics (no fields set)
|
||||
static size_t encode_telemetry_environment_metrics_empty(uint8_t *buffer, size_t buffer_size)
|
||||
{
|
||||
meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero;
|
||||
telemetry.time = 1609459200;
|
||||
telemetry.which_variant = meshtastic_Telemetry_environment_metrics_tag;
|
||||
|
||||
// NO fields are set - all has_* flags remain false
|
||||
// This tests that empty environment metrics don't produce any JSON fields
|
||||
|
||||
pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size);
|
||||
pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry);
|
||||
return stream.bytes_written;
|
||||
}
|
||||
|
||||
// Helper function to create environment metrics with ALL possible fields set
|
||||
// This function should be updated whenever new fields are added to the protobuf
|
||||
static size_t encode_telemetry_environment_metrics_all_fields(uint8_t *buffer, size_t buffer_size)
|
||||
{
|
||||
meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero;
|
||||
telemetry.time = 1609459200;
|
||||
telemetry.which_variant = meshtastic_Telemetry_environment_metrics_tag;
|
||||
|
||||
// Basic environment metrics
|
||||
telemetry.variant.environment_metrics.temperature = 23.56f;
|
||||
telemetry.variant.environment_metrics.has_temperature = true;
|
||||
telemetry.variant.environment_metrics.relative_humidity = 65.43f;
|
||||
telemetry.variant.environment_metrics.has_relative_humidity = true;
|
||||
telemetry.variant.environment_metrics.barometric_pressure = 1013.27f;
|
||||
telemetry.variant.environment_metrics.has_barometric_pressure = true;
|
||||
|
||||
// Gas and air quality
|
||||
telemetry.variant.environment_metrics.gas_resistance = 50.58f;
|
||||
telemetry.variant.environment_metrics.has_gas_resistance = true;
|
||||
telemetry.variant.environment_metrics.iaq = 120;
|
||||
telemetry.variant.environment_metrics.has_iaq = true;
|
||||
|
||||
// Power measurements
|
||||
telemetry.variant.environment_metrics.voltage = 3.34f;
|
||||
telemetry.variant.environment_metrics.has_voltage = true;
|
||||
telemetry.variant.environment_metrics.current = 0.53f;
|
||||
telemetry.variant.environment_metrics.has_current = true;
|
||||
|
||||
// Light measurements (ALL 4 types)
|
||||
telemetry.variant.environment_metrics.lux = 450.12f;
|
||||
telemetry.variant.environment_metrics.has_lux = true;
|
||||
telemetry.variant.environment_metrics.white_lux = 380.95f;
|
||||
telemetry.variant.environment_metrics.has_white_lux = true;
|
||||
telemetry.variant.environment_metrics.ir_lux = 25.37f;
|
||||
telemetry.variant.environment_metrics.has_ir_lux = true;
|
||||
telemetry.variant.environment_metrics.uv_lux = 15.68f;
|
||||
telemetry.variant.environment_metrics.has_uv_lux = true;
|
||||
|
||||
// Distance measurement
|
||||
telemetry.variant.environment_metrics.distance = 150.29f;
|
||||
telemetry.variant.environment_metrics.has_distance = true;
|
||||
|
||||
// Wind measurements (ALL 4 types)
|
||||
telemetry.variant.environment_metrics.wind_direction = 180;
|
||||
telemetry.variant.environment_metrics.has_wind_direction = true;
|
||||
telemetry.variant.environment_metrics.wind_speed = 5.52f;
|
||||
telemetry.variant.environment_metrics.has_wind_speed = true;
|
||||
telemetry.variant.environment_metrics.wind_gust = 8.24f;
|
||||
telemetry.variant.environment_metrics.has_wind_gust = true;
|
||||
telemetry.variant.environment_metrics.wind_lull = 2.13f;
|
||||
telemetry.variant.environment_metrics.has_wind_lull = true;
|
||||
|
||||
// Weight measurement
|
||||
telemetry.variant.environment_metrics.weight = 75.56f;
|
||||
telemetry.variant.environment_metrics.has_weight = true;
|
||||
|
||||
// Radiation measurement
|
||||
telemetry.variant.environment_metrics.radiation = 0.13f;
|
||||
telemetry.variant.environment_metrics.has_radiation = true;
|
||||
|
||||
// Rainfall measurements (BOTH types)
|
||||
telemetry.variant.environment_metrics.rainfall_1h = 2.57f;
|
||||
telemetry.variant.environment_metrics.has_rainfall_1h = true;
|
||||
telemetry.variant.environment_metrics.rainfall_24h = 15.89f;
|
||||
telemetry.variant.environment_metrics.has_rainfall_24h = true;
|
||||
|
||||
// Soil measurements (BOTH types)
|
||||
telemetry.variant.environment_metrics.soil_moisture = 85;
|
||||
telemetry.variant.environment_metrics.has_soil_moisture = true;
|
||||
telemetry.variant.environment_metrics.soil_temperature = 18.54f;
|
||||
telemetry.variant.environment_metrics.has_soil_temperature = true;
|
||||
|
||||
// IMPORTANT: When new environment fields are added to the protobuf,
|
||||
// they MUST be added here too, or the coverage test will fail!
|
||||
|
||||
pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size);
|
||||
pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry);
|
||||
return stream.bytes_written;
|
||||
}
|
||||
|
||||
// Helper function to create and encode environment metrics with all current fields
|
||||
static size_t encode_telemetry_environment_metrics(uint8_t *buffer, size_t buffer_size)
|
||||
{
|
||||
meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero;
|
||||
telemetry.time = 1609459200;
|
||||
telemetry.which_variant = meshtastic_Telemetry_environment_metrics_tag;
|
||||
|
||||
// Basic environment metrics
|
||||
telemetry.variant.environment_metrics.temperature = 23.56f;
|
||||
telemetry.variant.environment_metrics.has_temperature = true;
|
||||
telemetry.variant.environment_metrics.relative_humidity = 65.43f;
|
||||
telemetry.variant.environment_metrics.has_relative_humidity = true;
|
||||
telemetry.variant.environment_metrics.barometric_pressure = 1013.27f;
|
||||
telemetry.variant.environment_metrics.has_barometric_pressure = true;
|
||||
|
||||
// Gas and air quality
|
||||
telemetry.variant.environment_metrics.gas_resistance = 50.58f;
|
||||
telemetry.variant.environment_metrics.has_gas_resistance = true;
|
||||
telemetry.variant.environment_metrics.iaq = 120;
|
||||
telemetry.variant.environment_metrics.has_iaq = true;
|
||||
|
||||
// Power measurements
|
||||
telemetry.variant.environment_metrics.voltage = 3.34f;
|
||||
telemetry.variant.environment_metrics.has_voltage = true;
|
||||
telemetry.variant.environment_metrics.current = 0.53f;
|
||||
telemetry.variant.environment_metrics.has_current = true;
|
||||
|
||||
// Light measurements
|
||||
telemetry.variant.environment_metrics.lux = 450.12f;
|
||||
telemetry.variant.environment_metrics.has_lux = true;
|
||||
telemetry.variant.environment_metrics.white_lux = 380.95f;
|
||||
telemetry.variant.environment_metrics.has_white_lux = true;
|
||||
telemetry.variant.environment_metrics.ir_lux = 25.37f;
|
||||
telemetry.variant.environment_metrics.has_ir_lux = true;
|
||||
telemetry.variant.environment_metrics.uv_lux = 15.68f;
|
||||
telemetry.variant.environment_metrics.has_uv_lux = true;
|
||||
|
||||
// Distance measurement
|
||||
telemetry.variant.environment_metrics.distance = 150.29f;
|
||||
telemetry.variant.environment_metrics.has_distance = true;
|
||||
|
||||
// Wind measurements
|
||||
telemetry.variant.environment_metrics.wind_direction = 180;
|
||||
telemetry.variant.environment_metrics.has_wind_direction = true;
|
||||
telemetry.variant.environment_metrics.wind_speed = 5.52f;
|
||||
telemetry.variant.environment_metrics.has_wind_speed = true;
|
||||
telemetry.variant.environment_metrics.wind_gust = 8.24f;
|
||||
telemetry.variant.environment_metrics.has_wind_gust = true;
|
||||
telemetry.variant.environment_metrics.wind_lull = 2.13f;
|
||||
telemetry.variant.environment_metrics.has_wind_lull = true;
|
||||
|
||||
// Weight measurement
|
||||
telemetry.variant.environment_metrics.weight = 75.56f;
|
||||
telemetry.variant.environment_metrics.has_weight = true;
|
||||
|
||||
// Radiation measurement
|
||||
telemetry.variant.environment_metrics.radiation = 0.13f;
|
||||
telemetry.variant.environment_metrics.has_radiation = true;
|
||||
|
||||
// Rainfall measurements
|
||||
telemetry.variant.environment_metrics.rainfall_1h = 2.57f;
|
||||
telemetry.variant.environment_metrics.has_rainfall_1h = true;
|
||||
telemetry.variant.environment_metrics.rainfall_24h = 15.89f;
|
||||
telemetry.variant.environment_metrics.has_rainfall_24h = true;
|
||||
|
||||
// Soil measurements
|
||||
telemetry.variant.environment_metrics.soil_moisture = 85;
|
||||
telemetry.variant.environment_metrics.has_soil_moisture = true;
|
||||
telemetry.variant.environment_metrics.soil_temperature = 18.54f;
|
||||
telemetry.variant.environment_metrics.has_soil_temperature = true;
|
||||
fill_all_env_metrics(telemetry);
|
||||
|
||||
pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size);
|
||||
pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry);
|
||||
return stream.bytes_written;
|
||||
}
|
||||
|
||||
// Test TELEMETRY_APP port with device metrics
|
||||
static Json::Value serialize_and_get_payload(meshtastic_PortNum port, const uint8_t *buffer, size_t payload_size)
|
||||
{
|
||||
meshtastic_MeshPacket packet = create_test_packet(port, buffer, payload_size);
|
||||
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
|
||||
TEST_ASSERT_TRUE(json.length() > 0);
|
||||
|
||||
Json::Value root = parse_json(json);
|
||||
TEST_ASSERT_TRUE(root.isObject());
|
||||
TEST_ASSERT_TRUE(root.isMember("payload"));
|
||||
TEST_ASSERT_TRUE(root["payload"].isObject());
|
||||
return root;
|
||||
}
|
||||
|
||||
void test_telemetry_device_metrics_serialization()
|
||||
{
|
||||
uint8_t buffer[256];
|
||||
size_t payload_size = encode_telemetry_device_metrics(buffer, sizeof(buffer));
|
||||
|
||||
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
|
||||
Json::Value root = serialize_and_get_payload(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
|
||||
|
||||
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
|
||||
TEST_ASSERT_TRUE(json.length() > 0);
|
||||
TEST_ASSERT_TRUE(root.isMember("type"));
|
||||
TEST_ASSERT_EQUAL_STRING("telemetry", root["type"].asString().c_str());
|
||||
|
||||
JSONValue *root = JSON::Parse(json.c_str());
|
||||
TEST_ASSERT_NOT_NULL(root);
|
||||
TEST_ASSERT_TRUE(root->IsObject());
|
||||
const Json::Value &payload = root["payload"];
|
||||
|
||||
JSONObject jsonObj = root->AsObject();
|
||||
TEST_ASSERT_TRUE(payload.isMember("battery_level"));
|
||||
TEST_ASSERT_EQUAL(85, payload["battery_level"].asInt());
|
||||
|
||||
// Check message type
|
||||
TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end());
|
||||
TEST_ASSERT_EQUAL_STRING("telemetry", jsonObj["type"]->AsString().c_str());
|
||||
TEST_ASSERT_TRUE(payload.isMember("voltage"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 3.72f, payload["voltage"].asFloat());
|
||||
|
||||
// Check payload
|
||||
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
|
||||
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
|
||||
TEST_ASSERT_TRUE(payload.isMember("channel_utilization"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.56f, payload["channel_utilization"].asFloat());
|
||||
|
||||
JSONObject payload = jsonObj["payload"]->AsObject();
|
||||
|
||||
// Verify telemetry data
|
||||
TEST_ASSERT_TRUE(payload.find("battery_level") != payload.end());
|
||||
TEST_ASSERT_EQUAL(85, (int)payload["battery_level"]->AsNumber());
|
||||
|
||||
TEST_ASSERT_TRUE(payload.find("voltage") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 3.72f, payload["voltage"]->AsNumber());
|
||||
|
||||
TEST_ASSERT_TRUE(payload.find("channel_utilization") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.56f, payload["channel_utilization"]->AsNumber());
|
||||
|
||||
TEST_ASSERT_TRUE(payload.find("uptime_seconds") != payload.end());
|
||||
TEST_ASSERT_EQUAL(12345, (int)payload["uptime_seconds"]->AsNumber());
|
||||
|
||||
// Note: JSON serialization may not preserve exact 2-decimal formatting due to float precision
|
||||
// We verify the numeric values are correct within tolerance
|
||||
|
||||
delete root;
|
||||
TEST_ASSERT_TRUE(payload.isMember("uptime_seconds"));
|
||||
TEST_ASSERT_EQUAL(12345, payload["uptime_seconds"].asInt());
|
||||
}
|
||||
|
||||
// Test that telemetry environment metrics are properly serialized
|
||||
void test_telemetry_environment_metrics_serialization()
|
||||
{
|
||||
uint8_t buffer[256];
|
||||
size_t payload_size = encode_telemetry_environment_metrics(buffer, sizeof(buffer));
|
||||
|
||||
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
|
||||
Json::Value root = serialize_and_get_payload(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
|
||||
const Json::Value &payload = root["payload"];
|
||||
|
||||
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
|
||||
TEST_ASSERT_TRUE(json.length() > 0);
|
||||
TEST_ASSERT_TRUE(payload.isMember("temperature"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 23.56f, payload["temperature"].asFloat());
|
||||
|
||||
JSONValue *root = JSON::Parse(json.c_str());
|
||||
TEST_ASSERT_NOT_NULL(root);
|
||||
TEST_ASSERT_TRUE(root->IsObject());
|
||||
TEST_ASSERT_TRUE(payload.isMember("relative_humidity"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 65.43f, payload["relative_humidity"].asFloat());
|
||||
|
||||
JSONObject jsonObj = root->AsObject();
|
||||
|
||||
// Check payload exists
|
||||
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
|
||||
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
|
||||
|
||||
JSONObject payload = jsonObj["payload"]->AsObject();
|
||||
|
||||
// Test key fields that should be present in the serializer
|
||||
TEST_ASSERT_TRUE(payload.find("temperature") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 23.56f, payload["temperature"]->AsNumber());
|
||||
|
||||
TEST_ASSERT_TRUE(payload.find("relative_humidity") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 65.43f, payload["relative_humidity"]->AsNumber());
|
||||
|
||||
TEST_ASSERT_TRUE(payload.find("distance") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 150.29f, payload["distance"]->AsNumber());
|
||||
|
||||
// Note: JSON serialization may have float precision limitations
|
||||
// We focus on verifying numeric accuracy rather than exact string formatting
|
||||
|
||||
delete root;
|
||||
TEST_ASSERT_TRUE(payload.isMember("distance"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 150.29f, payload["distance"].asFloat());
|
||||
}
|
||||
|
||||
// Test comprehensive environment metrics coverage
|
||||
void test_telemetry_environment_metrics_comprehensive()
|
||||
{
|
||||
uint8_t buffer[256];
|
||||
size_t payload_size = encode_telemetry_environment_metrics(buffer, sizeof(buffer));
|
||||
|
||||
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
|
||||
Json::Value root = serialize_and_get_payload(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
|
||||
const Json::Value &payload = root["payload"];
|
||||
|
||||
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
|
||||
TEST_ASSERT_TRUE(json.length() > 0);
|
||||
|
||||
JSONValue *root = JSON::Parse(json.c_str());
|
||||
TEST_ASSERT_NOT_NULL(root);
|
||||
TEST_ASSERT_TRUE(root->IsObject());
|
||||
|
||||
JSONObject jsonObj = root->AsObject();
|
||||
|
||||
// Check payload exists
|
||||
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
|
||||
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
|
||||
|
||||
JSONObject payload = jsonObj["payload"]->AsObject();
|
||||
|
||||
// Check all 15 originally supported fields
|
||||
TEST_ASSERT_TRUE(payload.find("temperature") != payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("relative_humidity") != payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("barometric_pressure") != payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("gas_resistance") != payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("voltage") != payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("current") != payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("iaq") != payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("distance") != payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("lux") != payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("white_lux") != payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("wind_direction") != payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("wind_speed") != payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("wind_gust") != payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("wind_lull") != payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("radiation") != payload.end());
|
||||
|
||||
delete root;
|
||||
TEST_ASSERT_TRUE(payload.isMember("temperature"));
|
||||
TEST_ASSERT_TRUE(payload.isMember("relative_humidity"));
|
||||
TEST_ASSERT_TRUE(payload.isMember("barometric_pressure"));
|
||||
TEST_ASSERT_TRUE(payload.isMember("gas_resistance"));
|
||||
TEST_ASSERT_TRUE(payload.isMember("voltage"));
|
||||
TEST_ASSERT_TRUE(payload.isMember("current"));
|
||||
TEST_ASSERT_TRUE(payload.isMember("iaq"));
|
||||
TEST_ASSERT_TRUE(payload.isMember("distance"));
|
||||
TEST_ASSERT_TRUE(payload.isMember("lux"));
|
||||
TEST_ASSERT_TRUE(payload.isMember("white_lux"));
|
||||
TEST_ASSERT_TRUE(payload.isMember("wind_direction"));
|
||||
TEST_ASSERT_TRUE(payload.isMember("wind_speed"));
|
||||
TEST_ASSERT_TRUE(payload.isMember("wind_gust"));
|
||||
TEST_ASSERT_TRUE(payload.isMember("wind_lull"));
|
||||
TEST_ASSERT_TRUE(payload.isMember("radiation"));
|
||||
}
|
||||
|
||||
// Test for the 7 environment fields that were added to complete coverage
|
||||
void test_telemetry_environment_metrics_missing_fields()
|
||||
{
|
||||
uint8_t buffer[256];
|
||||
size_t payload_size = encode_telemetry_environment_metrics(buffer, sizeof(buffer));
|
||||
|
||||
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
|
||||
Json::Value root = serialize_and_get_payload(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
|
||||
const Json::Value &payload = root["payload"];
|
||||
|
||||
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
|
||||
TEST_ASSERT_TRUE(json.length() > 0);
|
||||
TEST_ASSERT_TRUE(payload.isMember("ir_lux"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 25.37f, payload["ir_lux"].asFloat());
|
||||
|
||||
JSONValue *root = JSON::Parse(json.c_str());
|
||||
TEST_ASSERT_NOT_NULL(root);
|
||||
TEST_ASSERT_TRUE(root->IsObject());
|
||||
TEST_ASSERT_TRUE(payload.isMember("uv_lux"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.68f, payload["uv_lux"].asFloat());
|
||||
|
||||
JSONObject jsonObj = root->AsObject();
|
||||
TEST_ASSERT_TRUE(payload.isMember("weight"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 75.56f, payload["weight"].asFloat());
|
||||
|
||||
// Check payload exists
|
||||
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
|
||||
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
|
||||
TEST_ASSERT_TRUE(payload.isMember("rainfall_1h"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.57f, payload["rainfall_1h"].asFloat());
|
||||
|
||||
JSONObject payload = jsonObj["payload"]->AsObject();
|
||||
TEST_ASSERT_TRUE(payload.isMember("rainfall_24h"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.89f, payload["rainfall_24h"].asFloat());
|
||||
|
||||
// Check the 7 fields that were previously missing
|
||||
TEST_ASSERT_TRUE(payload.find("ir_lux") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 25.37f, payload["ir_lux"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.isMember("soil_moisture"));
|
||||
TEST_ASSERT_EQUAL(85, payload["soil_moisture"].asInt());
|
||||
|
||||
TEST_ASSERT_TRUE(payload.find("uv_lux") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.68f, payload["uv_lux"]->AsNumber());
|
||||
|
||||
TEST_ASSERT_TRUE(payload.find("weight") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 75.56f, payload["weight"]->AsNumber());
|
||||
|
||||
TEST_ASSERT_TRUE(payload.find("rainfall_1h") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.57f, payload["rainfall_1h"]->AsNumber());
|
||||
|
||||
TEST_ASSERT_TRUE(payload.find("rainfall_24h") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.89f, payload["rainfall_24h"]->AsNumber());
|
||||
|
||||
TEST_ASSERT_TRUE(payload.find("soil_moisture") != payload.end());
|
||||
TEST_ASSERT_EQUAL(85, (int)payload["soil_moisture"]->AsNumber());
|
||||
|
||||
TEST_ASSERT_TRUE(payload.find("soil_temperature") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 18.54f, payload["soil_temperature"]->AsNumber());
|
||||
|
||||
// Note: JSON float serialization may not preserve exact decimal formatting
|
||||
// We verify the values are numerically correct within tolerance
|
||||
|
||||
delete root;
|
||||
TEST_ASSERT_TRUE(payload.isMember("soil_temperature"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 18.54f, payload["soil_temperature"].asFloat());
|
||||
}
|
||||
|
||||
// Test that ALL environment fields are serialized (canary test for forgotten fields)
|
||||
// This test will FAIL if a new environment field is added to the protobuf but not to the serializer
|
||||
// Canary test: if a new env field is added to the protobuf but not to the serializer
|
||||
// (or to fill_all_env_metrics), this test will fail.
|
||||
void test_telemetry_environment_metrics_complete_coverage()
|
||||
{
|
||||
uint8_t buffer[256];
|
||||
size_t payload_size = encode_telemetry_environment_metrics_all_fields(buffer, sizeof(buffer));
|
||||
size_t payload_size = encode_telemetry_environment_metrics(buffer, sizeof(buffer));
|
||||
|
||||
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
|
||||
Json::Value root = serialize_and_get_payload(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
|
||||
const Json::Value &payload = root["payload"];
|
||||
|
||||
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
|
||||
TEST_ASSERT_TRUE(json.length() > 0);
|
||||
TEST_ASSERT_TRUE(payload.isMember("temperature"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 23.56f, payload["temperature"].asFloat());
|
||||
TEST_ASSERT_TRUE(payload.isMember("relative_humidity"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 65.43f, payload["relative_humidity"].asFloat());
|
||||
TEST_ASSERT_TRUE(payload.isMember("barometric_pressure"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 1013.27f, payload["barometric_pressure"].asFloat());
|
||||
|
||||
JSONValue *root = JSON::Parse(json.c_str());
|
||||
TEST_ASSERT_NOT_NULL(root);
|
||||
TEST_ASSERT_TRUE(root->IsObject());
|
||||
TEST_ASSERT_TRUE(payload.isMember("gas_resistance"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 50.58f, payload["gas_resistance"].asFloat());
|
||||
TEST_ASSERT_TRUE(payload.isMember("iaq"));
|
||||
TEST_ASSERT_EQUAL(120, payload["iaq"].asInt());
|
||||
|
||||
JSONObject jsonObj = root->AsObject();
|
||||
TEST_ASSERT_TRUE(payload.isMember("voltage"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 3.34f, payload["voltage"].asFloat());
|
||||
TEST_ASSERT_TRUE(payload.isMember("current"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 0.53f, payload["current"].asFloat());
|
||||
|
||||
// Check payload exists
|
||||
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
|
||||
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
|
||||
TEST_ASSERT_TRUE(payload.isMember("lux"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 450.12f, payload["lux"].asFloat());
|
||||
TEST_ASSERT_TRUE(payload.isMember("white_lux"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 380.95f, payload["white_lux"].asFloat());
|
||||
TEST_ASSERT_TRUE(payload.isMember("ir_lux"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 25.37f, payload["ir_lux"].asFloat());
|
||||
TEST_ASSERT_TRUE(payload.isMember("uv_lux"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.68f, payload["uv_lux"].asFloat());
|
||||
|
||||
JSONObject payload = jsonObj["payload"]->AsObject();
|
||||
TEST_ASSERT_TRUE(payload.isMember("distance"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 150.29f, payload["distance"].asFloat());
|
||||
|
||||
// ✅ ALL 22 environment fields MUST be present and correct
|
||||
// If this test fails, it means either:
|
||||
// 1. A new field was added to the protobuf but not to the serializer
|
||||
// 2. The encode_telemetry_environment_metrics_all_fields() function wasn't updated
|
||||
TEST_ASSERT_TRUE(payload.isMember("wind_direction"));
|
||||
TEST_ASSERT_EQUAL(180, payload["wind_direction"].asInt());
|
||||
TEST_ASSERT_TRUE(payload.isMember("wind_speed"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 5.52f, payload["wind_speed"].asFloat());
|
||||
TEST_ASSERT_TRUE(payload.isMember("wind_gust"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 8.24f, payload["wind_gust"].asFloat());
|
||||
TEST_ASSERT_TRUE(payload.isMember("wind_lull"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.13f, payload["wind_lull"].asFloat());
|
||||
|
||||
// Basic environment (3 fields)
|
||||
TEST_ASSERT_TRUE(payload.find("temperature") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 23.56f, payload["temperature"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.find("relative_humidity") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 65.43f, payload["relative_humidity"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.find("barometric_pressure") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 1013.27f, payload["barometric_pressure"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.isMember("weight"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 75.56f, payload["weight"].asFloat());
|
||||
|
||||
// Gas and air quality (2 fields)
|
||||
TEST_ASSERT_TRUE(payload.find("gas_resistance") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 50.58f, payload["gas_resistance"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.find("iaq") != payload.end());
|
||||
TEST_ASSERT_EQUAL(120, (int)payload["iaq"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.isMember("radiation"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 0.13f, payload["radiation"].asFloat());
|
||||
|
||||
// Power measurements (2 fields)
|
||||
TEST_ASSERT_TRUE(payload.find("voltage") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 3.34f, payload["voltage"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.find("current") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 0.53f, payload["current"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.isMember("rainfall_1h"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.57f, payload["rainfall_1h"].asFloat());
|
||||
TEST_ASSERT_TRUE(payload.isMember("rainfall_24h"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.89f, payload["rainfall_24h"].asFloat());
|
||||
|
||||
// Light measurements (4 fields)
|
||||
TEST_ASSERT_TRUE(payload.find("lux") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 450.12f, payload["lux"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.find("white_lux") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 380.95f, payload["white_lux"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.find("ir_lux") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 25.37f, payload["ir_lux"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.find("uv_lux") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.68f, payload["uv_lux"]->AsNumber());
|
||||
|
||||
// Distance measurement (1 field)
|
||||
TEST_ASSERT_TRUE(payload.find("distance") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 150.29f, payload["distance"]->AsNumber());
|
||||
|
||||
// Wind measurements (4 fields)
|
||||
TEST_ASSERT_TRUE(payload.find("wind_direction") != payload.end());
|
||||
TEST_ASSERT_EQUAL(180, (int)payload["wind_direction"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.find("wind_speed") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 5.52f, payload["wind_speed"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.find("wind_gust") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 8.24f, payload["wind_gust"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.find("wind_lull") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.13f, payload["wind_lull"]->AsNumber());
|
||||
|
||||
// Weight measurement (1 field)
|
||||
TEST_ASSERT_TRUE(payload.find("weight") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 75.56f, payload["weight"]->AsNumber());
|
||||
|
||||
// Radiation measurement (1 field)
|
||||
TEST_ASSERT_TRUE(payload.find("radiation") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 0.13f, payload["radiation"]->AsNumber());
|
||||
|
||||
// Rainfall measurements (2 fields)
|
||||
TEST_ASSERT_TRUE(payload.find("rainfall_1h") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.57f, payload["rainfall_1h"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.find("rainfall_24h") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.89f, payload["rainfall_24h"]->AsNumber());
|
||||
|
||||
// Soil measurements (2 fields)
|
||||
TEST_ASSERT_TRUE(payload.find("soil_moisture") != payload.end());
|
||||
TEST_ASSERT_EQUAL(85, (int)payload["soil_moisture"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.find("soil_temperature") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 18.54f, payload["soil_temperature"]->AsNumber());
|
||||
|
||||
// Total: 22 environment fields
|
||||
// This test ensures 100% coverage of environment metrics
|
||||
|
||||
// Note: JSON float serialization precision may vary due to the underlying library
|
||||
// The important aspect is that all values are numerically accurate within tolerance
|
||||
|
||||
delete root;
|
||||
TEST_ASSERT_TRUE(payload.isMember("soil_moisture"));
|
||||
TEST_ASSERT_EQUAL(85, payload["soil_moisture"].asInt());
|
||||
TEST_ASSERT_TRUE(payload.isMember("soil_temperature"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 18.54f, payload["soil_temperature"].asFloat());
|
||||
}
|
||||
|
||||
// Test that unset environment fields are not present in JSON
|
||||
void test_telemetry_environment_metrics_unset_fields()
|
||||
{
|
||||
uint8_t buffer[256];
|
||||
size_t payload_size = encode_telemetry_environment_metrics_empty(buffer, sizeof(buffer));
|
||||
|
||||
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
|
||||
Json::Value root = serialize_and_get_payload(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
|
||||
const Json::Value &payload = root["payload"];
|
||||
|
||||
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
|
||||
TEST_ASSERT_TRUE(json.length() > 0);
|
||||
|
||||
JSONValue *root = JSON::Parse(json.c_str());
|
||||
TEST_ASSERT_NOT_NULL(root);
|
||||
TEST_ASSERT_TRUE(root->IsObject());
|
||||
|
||||
JSONObject jsonObj = root->AsObject();
|
||||
|
||||
// Check payload exists
|
||||
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
|
||||
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
|
||||
|
||||
JSONObject payload = jsonObj["payload"]->AsObject();
|
||||
|
||||
// With completely empty environment metrics, NO fields should be present
|
||||
// Only basic telemetry fields like "time" might be present
|
||||
|
||||
// All 22 environment fields should be absent (none were set)
|
||||
TEST_ASSERT_TRUE(payload.find("temperature") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("relative_humidity") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("barometric_pressure") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("gas_resistance") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("iaq") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("voltage") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("current") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("lux") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("white_lux") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("ir_lux") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("uv_lux") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("distance") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("wind_direction") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("wind_speed") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("wind_gust") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("wind_lull") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("weight") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("radiation") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("rainfall_1h") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("rainfall_24h") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("soil_moisture") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("soil_temperature") == payload.end());
|
||||
|
||||
delete root;
|
||||
TEST_ASSERT_FALSE(payload.isMember("temperature"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("relative_humidity"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("barometric_pressure"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("gas_resistance"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("iaq"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("voltage"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("current"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("lux"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("white_lux"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("ir_lux"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("uv_lux"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("distance"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("wind_direction"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("wind_speed"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("wind_gust"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("wind_lull"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("weight"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("radiation"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("rainfall_1h"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("rainfall_24h"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("soil_moisture"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("soil_temperature"));
|
||||
}
|
||||
|
||||
@@ -1,48 +1,30 @@
|
||||
#include "../test_helpers.h"
|
||||
#include <memory>
|
||||
|
||||
// Helper function to test common packet fields and structure
|
||||
void verify_text_message_packet_structure(const std::string &json, const char *expected_text)
|
||||
static void verify_text_message_packet_structure(const std::string &json, const char *expected_text)
|
||||
{
|
||||
TEST_ASSERT_TRUE(json.length() > 0);
|
||||
|
||||
// Use smart pointer for automatic memory management
|
||||
std::unique_ptr<JSONValue> root(JSON::Parse(json.c_str()));
|
||||
TEST_ASSERT_NOT_NULL(root.get());
|
||||
TEST_ASSERT_TRUE(root->IsObject());
|
||||
Json::Value root = parse_json(json);
|
||||
TEST_ASSERT_TRUE(root.isObject());
|
||||
|
||||
JSONObject jsonObj = root->AsObject();
|
||||
TEST_ASSERT_TRUE(root.isMember("from"));
|
||||
TEST_ASSERT_EQUAL(0x11223344u, root["from"].asUInt());
|
||||
TEST_ASSERT_TRUE(root.isMember("to"));
|
||||
TEST_ASSERT_EQUAL(0x55667788u, root["to"].asUInt());
|
||||
TEST_ASSERT_TRUE(root.isMember("id"));
|
||||
TEST_ASSERT_EQUAL(0x9999u, root["id"].asUInt());
|
||||
|
||||
// Check basic packet fields - use helper function to reduce duplication
|
||||
auto check_field = [&](const char *field, uint32_t expected_value) {
|
||||
auto it = jsonObj.find(field);
|
||||
TEST_ASSERT_TRUE(it != jsonObj.end());
|
||||
TEST_ASSERT_EQUAL(expected_value, (uint32_t)it->second->AsNumber());
|
||||
};
|
||||
TEST_ASSERT_TRUE(root.isMember("type"));
|
||||
TEST_ASSERT_EQUAL_STRING("text", root["type"].asString().c_str());
|
||||
|
||||
check_field("from", 0x11223344);
|
||||
check_field("to", 0x55667788);
|
||||
check_field("id", 0x9999);
|
||||
TEST_ASSERT_TRUE(root.isMember("payload"));
|
||||
TEST_ASSERT_TRUE(root["payload"].isObject());
|
||||
|
||||
// Check message type
|
||||
auto type_it = jsonObj.find("type");
|
||||
TEST_ASSERT_TRUE(type_it != jsonObj.end());
|
||||
TEST_ASSERT_EQUAL_STRING("text", type_it->second->AsString().c_str());
|
||||
|
||||
// Check payload
|
||||
auto payload_it = jsonObj.find("payload");
|
||||
TEST_ASSERT_TRUE(payload_it != jsonObj.end());
|
||||
TEST_ASSERT_TRUE(payload_it->second->IsObject());
|
||||
|
||||
JSONObject payload = payload_it->second->AsObject();
|
||||
auto text_it = payload.find("text");
|
||||
TEST_ASSERT_TRUE(text_it != payload.end());
|
||||
TEST_ASSERT_EQUAL_STRING(expected_text, text_it->second->AsString().c_str());
|
||||
|
||||
// No need for manual delete with smart pointer
|
||||
const Json::Value &payload = root["payload"];
|
||||
TEST_ASSERT_TRUE(payload.isMember("text"));
|
||||
TEST_ASSERT_EQUAL_STRING(expected_text, payload["text"].asString().c_str());
|
||||
}
|
||||
|
||||
// Test TEXT_MESSAGE_APP port
|
||||
void test_text_message_serialization()
|
||||
{
|
||||
const char *test_text = "Hello Meshtastic!";
|
||||
@@ -53,7 +35,6 @@ void test_text_message_serialization()
|
||||
verify_text_message_packet_structure(json, test_text);
|
||||
}
|
||||
|
||||
// Test with nullptr to check robustness
|
||||
void test_text_message_serialization_null()
|
||||
{
|
||||
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, nullptr, 0);
|
||||
@@ -62,11 +43,9 @@ void test_text_message_serialization_null()
|
||||
verify_text_message_packet_structure(json, "");
|
||||
}
|
||||
|
||||
// Test TEXT_MESSAGE_APP port with very long message (boundary testing)
|
||||
void test_text_message_serialization_long_text()
|
||||
{
|
||||
// Test with actual message size limits
|
||||
constexpr size_t MAX_MESSAGE_SIZE = 200; // Typical LoRa payload limit
|
||||
constexpr size_t MAX_MESSAGE_SIZE = 200;
|
||||
std::string long_text(MAX_MESSAGE_SIZE, 'A');
|
||||
|
||||
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP,
|
||||
@@ -76,30 +55,25 @@ void test_text_message_serialization_long_text()
|
||||
verify_text_message_packet_structure(json, long_text.c_str());
|
||||
}
|
||||
|
||||
// Test with message over size limit (should fail)
|
||||
void test_text_message_serialization_oversized()
|
||||
{
|
||||
constexpr size_t OVERSIZED_MESSAGE = 250; // Over the limit
|
||||
constexpr size_t OVERSIZED_MESSAGE = 250;
|
||||
std::string oversized_text(OVERSIZED_MESSAGE, 'B');
|
||||
|
||||
meshtastic_MeshPacket packet = create_test_packet(
|
||||
meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast<const uint8_t *>(oversized_text.c_str()), oversized_text.length());
|
||||
|
||||
// Should fail or return empty/error
|
||||
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
|
||||
// Should only verify first 234 characters for oversized messages
|
||||
std::string expected_text = oversized_text.substr(0, 234);
|
||||
verify_text_message_packet_structure(json, expected_text.c_str());
|
||||
}
|
||||
|
||||
// Add test for malformed UTF-8 sequences
|
||||
void test_text_message_serialization_invalid_utf8()
|
||||
{
|
||||
const uint8_t invalid_utf8[] = {0xFF, 0xFE, 0xFD, 0x00}; // Invalid UTF-8
|
||||
const uint8_t invalid_utf8[] = {0xFF, 0xFE, 0xFD, 0x00};
|
||||
meshtastic_MeshPacket packet =
|
||||
create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, invalid_utf8, sizeof(invalid_utf8) - 1);
|
||||
|
||||
// Should not crash, may produce replacement characters
|
||||
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
|
||||
TEST_ASSERT_TRUE(json.length() > 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ static size_t encode_waypoint(uint8_t *buffer, size_t buffer_size)
|
||||
waypoint.id = 12345;
|
||||
waypoint.latitude_i = 374208000;
|
||||
waypoint.longitude_i = -1221981000;
|
||||
waypoint.expire = 1609459200 + 3600; // 1 hour from now
|
||||
waypoint.expire = 1609459200 + 3600;
|
||||
strcpy(waypoint.name, "Test Point");
|
||||
strcpy(waypoint.description, "Test waypoint description");
|
||||
|
||||
@@ -15,7 +15,6 @@ static size_t encode_waypoint(uint8_t *buffer, size_t buffer_size)
|
||||
return stream.bytes_written;
|
||||
}
|
||||
|
||||
// Test WAYPOINT_APP port
|
||||
void test_waypoint_serialization()
|
||||
{
|
||||
uint8_t buffer[256];
|
||||
@@ -26,28 +25,20 @@ void test_waypoint_serialization()
|
||||
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
|
||||
TEST_ASSERT_TRUE(json.length() > 0);
|
||||
|
||||
JSONValue *root = JSON::Parse(json.c_str());
|
||||
TEST_ASSERT_NOT_NULL(root);
|
||||
TEST_ASSERT_TRUE(root->IsObject());
|
||||
Json::Value root = parse_json(json);
|
||||
TEST_ASSERT_TRUE(root.isObject());
|
||||
|
||||
JSONObject jsonObj = root->AsObject();
|
||||
TEST_ASSERT_TRUE(root.isMember("type"));
|
||||
TEST_ASSERT_EQUAL_STRING("waypoint", root["type"].asString().c_str());
|
||||
|
||||
// Check message type
|
||||
TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end());
|
||||
TEST_ASSERT_EQUAL_STRING("waypoint", jsonObj["type"]->AsString().c_str());
|
||||
TEST_ASSERT_TRUE(root.isMember("payload"));
|
||||
TEST_ASSERT_TRUE(root["payload"].isObject());
|
||||
|
||||
// Check payload
|
||||
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
|
||||
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
|
||||
const Json::Value &payload = root["payload"];
|
||||
|
||||
JSONObject payload = jsonObj["payload"]->AsObject();
|
||||
TEST_ASSERT_TRUE(payload.isMember("id"));
|
||||
TEST_ASSERT_EQUAL(12345, payload["id"].asInt());
|
||||
|
||||
// Verify waypoint data
|
||||
TEST_ASSERT_TRUE(payload.find("id") != payload.end());
|
||||
TEST_ASSERT_EQUAL(12345, (int)payload["id"]->AsNumber());
|
||||
|
||||
TEST_ASSERT_TRUE(payload.find("name") != payload.end());
|
||||
TEST_ASSERT_EQUAL_STRING("Test Point", payload["name"]->AsString().c_str());
|
||||
|
||||
delete root;
|
||||
TEST_ASSERT_TRUE(payload.isMember("name"));
|
||||
TEST_ASSERT_EQUAL_STRING("Test Point", payload["name"].asString().c_str());
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "serialization/JSON.h"
|
||||
#include "serialization/MeshPacketSerializer.h"
|
||||
#include <Arduino.h>
|
||||
#include <json/json.h>
|
||||
#include <memory>
|
||||
#include <meshtastic/mesh.pb.h>
|
||||
#include <meshtastic/mqtt.pb.h>
|
||||
#include <meshtastic/telemetry.pb.h>
|
||||
@@ -10,6 +11,18 @@
|
||||
#include <pb_encode.h>
|
||||
#include <unity.h>
|
||||
|
||||
// Parse a JSON string into a Json::Value; returns Json::nullValue on failure.
|
||||
static inline Json::Value parse_json(const std::string &s)
|
||||
{
|
||||
Json::CharReaderBuilder b;
|
||||
Json::Value root;
|
||||
std::string errs;
|
||||
std::unique_ptr<Json::CharReader> reader(b.newCharReader());
|
||||
if (!reader->parse(s.c_str(), s.c_str() + s.size(), &root, &errs))
|
||||
return Json::Value();
|
||||
return root;
|
||||
}
|
||||
|
||||
// Helper function to create a test packet with the given port and payload
|
||||
static meshtastic_MeshPacket create_test_packet(meshtastic_PortNum port, const uint8_t *payload, size_t payload_size,
|
||||
int payload_variant = meshtastic_MeshPacket_decoded_tag)
|
||||
@@ -36,7 +49,8 @@ static meshtastic_MeshPacket create_test_packet(meshtastic_PortNum port, const u
|
||||
packet.encrypted.size = payload_size;
|
||||
memcpy(packet.encrypted.bytes, payload, packet.encrypted.size);
|
||||
}
|
||||
memcpy(packet.decoded.payload.bytes, payload, payload_size);
|
||||
if (payload && payload_size)
|
||||
memcpy(packet.decoded.payload.bytes, payload, payload_size);
|
||||
packet.decoded.payload.size = payload_size;
|
||||
packet.decoded.want_response = false;
|
||||
packet.decoded.dest = 0x55667788;
|
||||
|
||||
@@ -27,12 +27,6 @@
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
#if defined(UNIT_TEST)
|
||||
#define IS_RUNNING_TESTS 1
|
||||
#else
|
||||
#define IS_RUNNING_TESTS 0
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
// Minimal router needed to receive messages from MQTT.
|
||||
|
||||
@@ -33,7 +33,12 @@ class TestableRadioInterface : public RadioInterface
|
||||
|
||||
static void test_bwCodeToKHz_specialMappings()
|
||||
{
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 7.8f, bwCodeToKHz(8));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 10.4f, bwCodeToKHz(10));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 15.6f, bwCodeToKHz(16));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 20.8f, bwCodeToKHz(21));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 31.25f, bwCodeToKHz(31));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 41.7f, bwCodeToKHz(42));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 62.5f, bwCodeToKHz(62));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 203.125f, bwCodeToKHz(200));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 406.25f, bwCodeToKHz(400));
|
||||
@@ -47,6 +52,18 @@ static void test_bwCodeToKHz_passthrough()
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 250.0f, bwCodeToKHz(250));
|
||||
}
|
||||
|
||||
static void test_bwCodeToKHz_roundTrip()
|
||||
{
|
||||
// Round-trip: bwKHzToCode(bwCodeToKHz(code)) should return the original code
|
||||
uint16_t codes[] = {8, 10, 16, 21, 31, 42, 62, 200, 400, 800, 1600};
|
||||
for (size_t i = 0; i < sizeof(codes) / sizeof(codes[0]); i++) {
|
||||
uint16_t code = codes[i];
|
||||
float khz = bwCodeToKHz(code);
|
||||
uint16_t result = bwKHzToCode(khz);
|
||||
TEST_ASSERT_EQUAL_UINT16(code, result);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_validateConfigLora_noopWhenUsePresetFalse()
|
||||
{
|
||||
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
|
||||
@@ -213,6 +230,7 @@ void setup()
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_bwCodeToKHz_specialMappings);
|
||||
RUN_TEST(test_bwCodeToKHz_passthrough);
|
||||
RUN_TEST(test_bwCodeToKHz_roundTrip);
|
||||
RUN_TEST(test_validateConfigLora_noopWhenUsePresetFalse);
|
||||
RUN_TEST(test_validateConfigLora_validPreset_nonWideRegion);
|
||||
RUN_TEST(test_validateConfigLora_validPreset_wideRegion);
|
||||
|
||||
@@ -5,12 +5,6 @@
|
||||
#ifdef ARCH_PORTDUINO
|
||||
#include "configuration.h"
|
||||
|
||||
#if defined(UNIT_TEST)
|
||||
#define IS_RUNNING_TESTS 1
|
||||
#else
|
||||
#define IS_RUNNING_TESTS 0
|
||||
#endif
|
||||
|
||||
#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \
|
||||
!defined(CONFIG_IDF_TARGET_ESP32C3)
|
||||
#include "modules/SerialModule.h"
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
#ifndef Pins_Arduino_h
|
||||
#define Pins_Arduino_h
|
||||
|
||||
#include "soc/soc_caps.h"
|
||||
#include <stdint.h>
|
||||
|
||||
// BOOT_MODE 35
|
||||
// BOOT_MODE2 36 pullup
|
||||
|
||||
static const uint8_t TX = 37;
|
||||
static const uint8_t RX = 38;
|
||||
|
||||
#if defined(CROWPANEL_ADV_P4_50)
|
||||
static const uint8_t SDA = 45;
|
||||
static const uint8_t SCL = 46;
|
||||
#else
|
||||
static const uint8_t SDA = 45;
|
||||
static const uint8_t SCL = 46;
|
||||
#endif
|
||||
|
||||
// Use GPIOs 36 or lower on the P4 DevKit to avoid LDO power issues with high numbered GPIOs.
|
||||
static const uint8_t SS = 30;
|
||||
static const uint8_t MOSI = 48;
|
||||
static const uint8_t MISO = 47;
|
||||
static const uint8_t SCK = 26;
|
||||
|
||||
static const uint8_t A0 = 16;
|
||||
static const uint8_t A1 = 17;
|
||||
static const uint8_t A2 = 18;
|
||||
static const uint8_t A3 = 19;
|
||||
static const uint8_t A4 = 20;
|
||||
static const uint8_t A5 = 21;
|
||||
static const uint8_t A6 = 22;
|
||||
static const uint8_t A7 = 23;
|
||||
static const uint8_t A8 = 49;
|
||||
static const uint8_t A9 = 50;
|
||||
static const uint8_t A10 = 51;
|
||||
static const uint8_t A11 = 52;
|
||||
static const uint8_t A12 = 53;
|
||||
static const uint8_t A13 = 54;
|
||||
|
||||
static const uint8_t T0 = 2;
|
||||
static const uint8_t T1 = 3;
|
||||
static const uint8_t T2 = 4;
|
||||
static const uint8_t T3 = 5;
|
||||
static const uint8_t T4 = 6;
|
||||
static const uint8_t T5 = 7;
|
||||
static const uint8_t T6 = 8;
|
||||
static const uint8_t T7 = 9;
|
||||
static const uint8_t T8 = 10;
|
||||
static const uint8_t T9 = 11;
|
||||
static const uint8_t T10 = 12;
|
||||
static const uint8_t T11 = 13;
|
||||
static const uint8_t T12 = 14;
|
||||
static const uint8_t T13 = 15;
|
||||
|
||||
/* ESP32-P4 EV Function board specific definitions */
|
||||
// ETH
|
||||
// #define ETH_PHY_TYPE ETH_PHY_TLK110
|
||||
// #define ETH_PHY_ADDR 1
|
||||
// #define ETH_PHY_MDC 31
|
||||
// #define ETH_PHY_MDIO 52
|
||||
// #define ETH_PHY_POWER 51
|
||||
// #define ETH_RMII_TX_EN 49
|
||||
// #define ETH_RMII_TX0 34
|
||||
// #define ETH_RMII_TX1 35
|
||||
// #define ETH_RMII_RX0 29
|
||||
// #define ETH_RMII_RX1_EN 30
|
||||
// #define ETH_RMII_CRS_DV 28
|
||||
// #define ETH_RMII_CLK 50
|
||||
// #define ETH_CLK_MODE EMAC_CLK_EXT_IN
|
||||
|
||||
// SDMMC
|
||||
#define BOARD_HAS_SDMMC
|
||||
#define BOARD_HAS_SD_SDMMC
|
||||
#define SDMMC_CMD 44 // SD_CMD/MOSI
|
||||
#define SDMMC_CLK 43 // SD_CLK
|
||||
#define SDMMC_D0 39 // SD_D0/MISO
|
||||
#define SD_CS -1 // No CS pin in SDMMC mode
|
||||
|
||||
#if defined(CROWPANEL_ADV_P4_50)
|
||||
#define BOARD_SDMMC_SLOT 1
|
||||
// Workaround for Arduino-ESP32 P4 SPI LDO auto-config on SDMMC slot0 pins (47/48).
|
||||
// Use a valid GPIO (aligned to variant LoRa CS) and pre-tag it in initVariant()
|
||||
// so setLDOPower() short-circuits.
|
||||
#define BOARD_SDMMC_POWER_PIN 30
|
||||
#else
|
||||
#define BOARD_SDMMC_POWER_PIN 10
|
||||
#define BOARD_SDMMC_SLOT 0
|
||||
#endif
|
||||
#define BOARD_SDMMC_POWER_CHANNEL 4
|
||||
#define BOARD_SDMMC_POWER_ON_LEVEL HIGH
|
||||
|
||||
// BT/WIFI - ESP32C6
|
||||
#define BOARD_HAS_SDIO_ESP_HOSTED
|
||||
#ifdef CROWPANEL_ADV_P4_50
|
||||
// CrowPanel Advanced P4 50": 4-bit SDIO on Slot 1 with GPIO 53/54/52/51/50/49
|
||||
#define BOARD_SDIO_ESP_HOSTED_CLK 53
|
||||
#define BOARD_SDIO_ESP_HOSTED_CMD 54
|
||||
#define BOARD_SDIO_ESP_HOSTED_D0 52
|
||||
#define BOARD_SDIO_ESP_HOSTED_D1 51
|
||||
#define BOARD_SDIO_ESP_HOSTED_D2 50
|
||||
#define BOARD_SDIO_ESP_HOSTED_D3 49
|
||||
#define BOARD_SDIO_ESP_HOSTED_RESET 20
|
||||
#else
|
||||
// CrowPanel Advanced P4 70/90/101": 1-bit SDIO on Slot 1 with GPIO 18/19/14/15
|
||||
#define BOARD_SDIO_ESP_HOSTED_CLK 18
|
||||
#define BOARD_SDIO_ESP_HOSTED_CMD 19
|
||||
#define BOARD_SDIO_ESP_HOSTED_D0 14
|
||||
#define BOARD_SDIO_ESP_HOSTED_D1 15
|
||||
#define BOARD_SDIO_ESP_HOSTED_D2 16
|
||||
#define BOARD_SDIO_ESP_HOSTED_D3 17
|
||||
#define BOARD_SDIO_ESP_HOSTED_RESET 32
|
||||
#endif
|
||||
|
||||
#endif /* Pins_Arduino_h */
|
||||
@@ -0,0 +1,194 @@
|
||||
[env:crowpanel-advanced-p4-50]
|
||||
extends = esp32p4_base
|
||||
board = crowpanel-p4
|
||||
board_check = true
|
||||
board_build.partitions = default_16MB.csv
|
||||
|
||||
build_src_filter =
|
||||
${esp32p4_base.build_src_filter}
|
||||
+<../variants/esp32p4/crowpanel-advanced-p4>
|
||||
|
||||
build_flags =
|
||||
${esp32p4_base.build_flags}
|
||||
-D CROWPANEL_ADV_P4
|
||||
-D CROWPANEL_ADV_P4_50
|
||||
-I variants/esp32p4/crowpanel-advanced-p4
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=0
|
||||
-D ARDUINO_USB_MODE=1
|
||||
-D MESHTASTIC_EXCLUDE_WEBSERVER=1
|
||||
-D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1
|
||||
-D MESHTASTIC_EXCLUDE_INPUTBROKER=1
|
||||
-D MAX_NUM_NODES=500
|
||||
-D MAX_NUM_NODES_VIEW=500
|
||||
; -DSOC_USB_OTG_SUPPORTED=1
|
||||
-D CONFIG_DISABLE_HAL_LOCKS=1 ; "feels" to be a bit more stable without locks
|
||||
; -D INPUTDRIVER_BUTTON_TYPE=0
|
||||
-D HAS_SDCARD
|
||||
-D HAS_SD_MMC
|
||||
-D BOARD_MAX_SDMMC_FREQ=100000
|
||||
-D BOARD_HAS_1BIT_SDMMC
|
||||
-D SD_SCLK_PIN=43
|
||||
-D SD_MISO_PIN=39
|
||||
-D SD_MOSI_PIN=44
|
||||
-D SD_MMC_HOST_SLOT=SDMMC_HOST_SLOT_1
|
||||
-D HAS_SCREEN=1
|
||||
-D HAS_TFT=1
|
||||
; -D USE_I2S_BUZZER
|
||||
-D RAM_SIZE=10240
|
||||
-D LV_LVGL_H_INCLUDE_SIMPLE
|
||||
-D LV_CONF_INCLUDE_SIMPLE
|
||||
-D LV_COMP_CONF_INCLUDE_SIMPLE
|
||||
-D LV_USE_SYSMON=0
|
||||
-D LV_USE_PROFILER=0
|
||||
-D LV_USE_PERF_MONITOR=0
|
||||
-D LV_USE_MEM_MONITOR=0
|
||||
-D LV_USE_LOG=0
|
||||
-D LV_CACHE_DEF_SIZE=3400000
|
||||
-D USE_LOG_DEBUG
|
||||
-D LOG_DEBUG_INC=\"DebugConfiguration.h\"
|
||||
-D RADIOLIB_SPI_PARANOID=0
|
||||
-D LGFX_SCREEN_WIDTH=800
|
||||
-D LGFX_SCREEN_HEIGHT=480
|
||||
-D DISPLAY_SIZE=800x480 ; landscape mode
|
||||
-D LGFX_DRIVER=LGFX_ELECROW_P4_50
|
||||
-D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_ELECROW_P4_50.h\"
|
||||
-D LGFX_BUFSIZE=1152000
|
||||
-D LGFX_SKIP_RGB565_SWAP
|
||||
-D VIEW_320x240
|
||||
-D DISPLAY_SET_RESOLUTION
|
||||
-D USE_PACKET_API
|
||||
-D MAP_FULL_REDRAW
|
||||
|
||||
lib_deps =
|
||||
${esp32p4_base.lib_deps}
|
||||
${device-ui_base.lib_deps}
|
||||
# renovate: datasource=github-tags depName=LovyanGFX packageName=lovyan03/LovyanGFX
|
||||
https://github.com/lovyan03/LovyanGFX/archive/refs/tags/1.2.21.zip
|
||||
|
||||
custom_sdkconfig =
|
||||
${esp32p4_base.custom_sdkconfig}
|
||||
# Disable external SDMMC IO power control to avoid GPIO conflicts
|
||||
CONFIG_SOC_SDMMC_IO_POWER_EXTERNAL=n
|
||||
# CONFIG_BT_BLE_50_FEATURES_SUPPORTED is not set
|
||||
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
|
||||
|
||||
# Force hosted transport/target selection for this env.
|
||||
CONFIG_ESP_HOSTED_SDIO_4_BIT_BUS=y
|
||||
CONFIG_ESP_HOSTED_SDIO_CLOCK_FREQ_KHZ=40000
|
||||
# CrowPanel SDIO pins
|
||||
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CMD_SLOT_1=54
|
||||
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CLK_SLOT_1=53
|
||||
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D0_SLOT_1=52
|
||||
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D1_4BIT_BUS_SLOT_1=51
|
||||
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D2_4BIT_BUS_SLOT_1=50
|
||||
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D3_4BIT_BUS_SLOT_1=49
|
||||
|
||||
# Also set resolved SDIO pins explicitly so generated sdkconfig does not
|
||||
# retain stale template values from framework sdkconfig.
|
||||
CONFIG_ESP_HOSTED_SDIO_PIN_CMD=54
|
||||
CONFIG_ESP_HOSTED_SDIO_PIN_CLK=53
|
||||
CONFIG_ESP_HOSTED_SDIO_PIN_D0=52
|
||||
CONFIG_ESP_HOSTED_SDIO_PIN_D1=51
|
||||
CONFIG_ESP_HOSTED_SDIO_PIN_D2=50
|
||||
CONFIG_ESP_HOSTED_SDIO_PIN_D3=49
|
||||
CONFIG_ESP_HOSTED_SDIO_PRIV_PIN_D1_4BIT_BUS=51
|
||||
|
||||
# SDIO Slot 1 via GPIO matrix
|
||||
CONFIG_ESP_HOSTED_SDIO_SLOT_1=y
|
||||
CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_HIGH=y
|
||||
CONFIG_ESP_HOSTED_SDIO_GPIO_RESET_SLAVE=20
|
||||
CONFIG_ESP_HOSTED_GPIO_SLAVE_RESET_SLAVE=20
|
||||
|
||||
[env:crowpanel-advanced-p4-70-90-101]
|
||||
extends = esp32p4_base
|
||||
board = esp32-p4-evboard
|
||||
board_check = true
|
||||
board_build.partitions = default_16MB.csv
|
||||
|
||||
build_src_filter =
|
||||
${esp32p4_base.build_src_filter}
|
||||
+<../variants/esp32p4/crowpanel-advanced-p4>
|
||||
|
||||
build_flags =
|
||||
${esp32p4_base.build_flags}
|
||||
-D CROWPANEL_ADV_P4
|
||||
-D CROWPANEL_ADV_P4_70_90_101
|
||||
-I variants/esp32p4/crowpanel-advanced-p4
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=0
|
||||
-D ARDUINO_USB_MODE=1
|
||||
-D MESHTASTIC_EXCLUDE_WEBSERVER=1
|
||||
-D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1
|
||||
-D MESHTASTIC_EXCLUDE_INPUTBROKER=1
|
||||
-D MAX_NUM_NODES=500
|
||||
-D MAX_NUM_NODES_VIEW=500
|
||||
; -DSOC_USB_OTG_SUPPORTED=1
|
||||
-D CONFIG_DISABLE_HAL_LOCKS=1 ; "feels" to be a bit more stable without locks
|
||||
; -D INPUTDRIVER_BUTTON_TYPE=0
|
||||
-D HAS_SDCARD
|
||||
-D HAS_SD_MMC
|
||||
-D BOARD_MAX_SDMMC_FREQ=10000
|
||||
-D BOARD_HAS_1BIT_SDMMC
|
||||
-D SD_SCLK_PIN=43
|
||||
-D SD_MISO_PIN=39
|
||||
-D SD_MOSI_PIN=44
|
||||
-D SD_MMC_HOST_SLOT=SDMMC_HOST_SLOT_0
|
||||
-D HAS_SCREEN=1
|
||||
-D HAS_TFT=1
|
||||
; -D USE_I2S_BUZZER
|
||||
-D RAM_SIZE=12288
|
||||
-D LV_LVGL_H_INCLUDE_SIMPLE
|
||||
-D LV_CONF_INCLUDE_SIMPLE
|
||||
-D LV_COMP_CONF_INCLUDE_SIMPLE
|
||||
-D LV_USE_SYSMON=1
|
||||
-D LV_USE_PROFILER=0
|
||||
-D LV_USE_PERF_MONITOR=1
|
||||
-D LV_USE_MEM_MONITOR=0
|
||||
-D LV_USE_LOG=0
|
||||
-D LV_CACHE_DEF_SIZE=5600000
|
||||
-D USE_LOG_DEBUG
|
||||
-D LOG_DEBUG_INC=\"DebugConfiguration.h\"
|
||||
-D LORA_SPI_FREQUENCY=1000000
|
||||
-D BOARD_LORA_MAX_TX_POWER=17
|
||||
-D SX126X_CURRENT_LIMIT_MA=100
|
||||
-D RADIOLIB_SPI_PARANOID=0
|
||||
-D LGFX_SCREEN_WIDTH=1024
|
||||
-D LGFX_SCREEN_HEIGHT=600
|
||||
-D DISPLAY_SIZE=1024x600 ; landscape mode
|
||||
-D LGFX_DRIVER=LGFX_ELECROW_P4_70_90_101
|
||||
-D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_ELECROW_P4_70_90_101.h\"
|
||||
-D LGFX_BUFSIZE=614400
|
||||
-D VIEW_320x240
|
||||
-D DISPLAY_SET_RESOLUTION
|
||||
-D USE_PACKET_API
|
||||
; -D MAP_FULL_REDRAW
|
||||
|
||||
lib_deps =
|
||||
${esp32p4_base.lib_deps}
|
||||
${device-ui_base.lib_deps}
|
||||
https://github.com/lovyan03/LovyanGFX#develop
|
||||
|
||||
custom_sdkconfig =
|
||||
${esp32p4_base.custom_sdkconfig}
|
||||
CONFIG_SOC_SDMMC_IO_POWER_EXTERNAL=n
|
||||
# CONFIG_BT_BLE_50_FEATURES_SUPPORTED is not set
|
||||
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
|
||||
# SDIO Slot 1 via GPIO matrix
|
||||
CONFIG_ESP_HOSTED_SDIO_SLOT_1=y
|
||||
|
||||
# 1-bit bus (proven stable on CrowPanel)
|
||||
CONFIG_ESP_HOSTED_SDIO_1_BIT_BUS=y
|
||||
CONFIG_ESP_HOSTED_SDIO_BUS_WIDTH=1
|
||||
|
||||
# Conservative 10 MHz clock for OTA reliability
|
||||
CONFIG_ESP_HOSTED_SDIO_CLOCK_FREQ_KHZ=10000
|
||||
|
||||
# CrowPanel SDIO pins (happen to match P4 Slot 1 defaults)
|
||||
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CLK_SLOT_1=18
|
||||
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CMD_SLOT_1=19
|
||||
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D0_SLOT_1=14
|
||||
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D1_1BIT_BUS_SLOT_1=15
|
||||
|
||||
# Reset pin: GPIO32, active high (R77 pullup on CrowPanel)
|
||||
CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_HIGH=y
|
||||
CONFIG_ESP_HOSTED_SDIO_GPIO_RESET_SLAVE=32
|
||||
CONFIG_ESP_HOSTED_GPIO_SLAVE_RESET_SLAVE=32
|
||||
@@ -0,0 +1,23 @@
|
||||
#include "variant.h"
|
||||
#include "Arduino.h"
|
||||
#include <esp32-hal-periman.h>
|
||||
|
||||
extern "C" void initVariant(void)
|
||||
{
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32P4) && defined(BOARD_SDMMC_POWER_PIN)
|
||||
// Ensure setLDOPower() exits early for this board family.
|
||||
// On ESP32-P4, Arduino SPI may attempt SDMMC on-chip LDO setup when SPI uses
|
||||
// pins that match SDMMC slot0 IOMUX pins (for example, GPIO 47/48 on p4-50).
|
||||
// Pre-tagging BOARD_SDMMC_POWER_PIN as "SDMMC POWER" short-circuits that path.
|
||||
if (perimanPinIsValid(BOARD_SDMMC_POWER_PIN)) {
|
||||
pinMode(BOARD_SDMMC_POWER_PIN, OUTPUT);
|
||||
digitalWrite(BOARD_SDMMC_POWER_PIN, BOARD_SDMMC_POWER_ON_LEVEL);
|
||||
|
||||
if (perimanGetPinBusType(BOARD_SDMMC_POWER_PIN) != ESP32_BUS_TYPE_MAX &&
|
||||
perimanGetPinBusExtraType(BOARD_SDMMC_POWER_PIN) == nullptr) {
|
||||
perimanSetPinBusExtraType(BOARD_SDMMC_POWER_PIN, "");
|
||||
}
|
||||
perimanSetPinBusExtraType(BOARD_SDMMC_POWER_PIN, "SDMMC POWER");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#define HAS_WIRE 0
|
||||
#define I2C_SDA1 45
|
||||
#define I2C_SCL1 46
|
||||
|
||||
#define USE_POWERSAVE
|
||||
#define WAKE_ON_TOUCH
|
||||
#define SCREEN_TOUCH_INT 42
|
||||
#define SLEEP_TIME 180
|
||||
|
||||
#if defined(CROWPANEL_ADV_P4_50)
|
||||
|
||||
// use UART3-IN for GPS (UART1 can not work with lora)
|
||||
#define GPS_DEFAULT_NOT_PRESENT 1
|
||||
#define GPS_RX_PIN 28
|
||||
#define GPS_TX_PIN 27
|
||||
|
||||
// LoRa
|
||||
#define USE_SX1262
|
||||
#define LORA_SCK 26
|
||||
#define LORA_MISO 47
|
||||
#define LORA_MOSI 48
|
||||
#define LORA_CS 30
|
||||
#define LORA_RESET 32
|
||||
|
||||
#define SX126X_CS LORA_CS
|
||||
#define SX126X_DIO1 31
|
||||
#define SX126X_BUSY 29
|
||||
#define SX126X_RESET LORA_RESET
|
||||
#define SX126X_DIO2_AS_RF_SWITCH
|
||||
#define SX126X_DIO3_TCXO_VOLTAGE 3.3
|
||||
|
||||
#elif defined(CROWPANEL_ADV_P4_70_90_101)
|
||||
|
||||
// use UART1 for GPS
|
||||
#define GPS_DEFAULT_NOT_PRESENT 1
|
||||
#define GPS_RX_PIN 48
|
||||
#define GPS_TX_PIN 47
|
||||
|
||||
// LoRa
|
||||
#define USE_SX1262
|
||||
#define LORA_SCK 8
|
||||
#define LORA_MISO 7
|
||||
#define LORA_MOSI 6
|
||||
#define LORA_CS 10
|
||||
#define LORA_RESET 54
|
||||
|
||||
#define SX126X_CS LORA_CS
|
||||
#define SX126X_DIO1 53
|
||||
#define SX126X_BUSY 9
|
||||
#define SX126X_RESET LORA_RESET
|
||||
#define SX126X_DIO2_AS_RF_SWITCH
|
||||
#define SX126X_DIO3_TCXO_VOLTAGE 3.3
|
||||
|
||||
#endif
|
||||
@@ -11,7 +11,7 @@ build_flags = -fno-strict-aliasing
|
||||
-D USE_EINK
|
||||
-D USE_EINK_PARALLELDISPLAY
|
||||
-D PRIVATE_HW
|
||||
-D TOUCH_THRESHOLD_X=60
|
||||
-D TOUCH_THRESHOLD_X=40
|
||||
-D TOUCH_THRESHOLD_Y=40
|
||||
-D TIME_LONG_PRESS=500
|
||||
; -D EINK_LIMIT_GHOSTING_PX=5000
|
||||
|
||||
@@ -53,6 +53,9 @@ build_flags_common =
|
||||
-DRADIOLIB_EEPROM_UNSUPPORTED
|
||||
-lpthread
|
||||
-lyaml-cpp
|
||||
-ljsoncpp
|
||||
!pkg-config --cflags jsoncpp --silence-errors || :
|
||||
-li2c
|
||||
-luv
|
||||
-std=gnu17
|
||||
-std=gnu++17
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
#ifndef HAS_TRAFFIC_MANAGEMENT
|
||||
#define HAS_TRAFFIC_MANAGEMENT 1
|
||||
#endif
|
||||
#ifndef HAS_VARIABLE_HOPS
|
||||
#define HAS_VARIABLE_HOPS 1
|
||||
#endif
|
||||
#ifndef TRAFFIC_MANAGEMENT_CACHE_SIZE
|
||||
#define TRAFFIC_MANAGEMENT_CACHE_SIZE 2048
|
||||
#endif
|
||||
|
||||
@@ -10,7 +10,6 @@ build_flags = ${nrf52840_base.build_flags}
|
||||
-DEINK_DISPLAY_MODEL=GxEPD2_213_BN
|
||||
-DEINK_WIDTH=250
|
||||
-DEINK_HEIGHT=122
|
||||
-DNRF52_USE_JSON=1
|
||||
-DMESHTASTIC_EXCLUDE_WIFI=1
|
||||
-DMESHTASTIC_EXCLUDE_SCREEN=1
|
||||
; -DMESHTASTIC_EXCLUDE_PKI=1
|
||||
@@ -34,8 +33,6 @@ lib_deps =
|
||||
rakwireless/RAKwireless NCP5623 RGB LED library@1.0.3
|
||||
# renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main
|
||||
https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip
|
||||
# renovate: datasource=custom.pio depName=ArduinoJson packageName=bblanchon/library/ArduinoJson
|
||||
bblanchon/ArduinoJson@6.21.6
|
||||
; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm)
|
||||
; Note: as of 6/2013 the serial/bootloader based programming takes approximately 30 seconds
|
||||
;upload_protocol = jlink
|
||||
|
||||
Reference in New Issue
Block a user