Compare commits

..
113 changed files with 3688 additions and 5977 deletions
+1 -1
View File
@@ -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 libjsoncpp-dev libbluetooth-dev libi2c-dev \
cmake git zip libgpiod-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/* && \
+1 -1
View File
@@ -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 jsoncpp bluez --silence-errors)
STATIC_LIBS=$(pkg-config --libs --static libulfius openssl libgpiod yaml-cpp bluez --silence-errors)
export PLATFORMIO_EXTRA_SCRIPTS
export STATIC_LIBS
export PLATFORMIO_WORKSPACE_DIR="$WORK/pio/$SANITIZER"
-1
View File
@@ -16,7 +16,6 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
libssl-dev \
libulfius-dev \
libyaml-cpp-dev \
libjsoncpp-dev \
pipx \
pkg-config \
python3 \
+1 -1
View File
@@ -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 libjsoncpp-dev lsb-release
sudo apt-get install -y cppcheck libbluetooth-dev libgpiod-dev libyaml-cpp-dev lsb-release
- name: Setup Python
uses: actions/setup-python@v6
+1 -1
View File
@@ -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 libjsoncpp-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 openssl libssl-dev libulfius-dev liborcania-dev libusb-1.0-0-dev libi2c-dev libuv1-dev
+8 -23
View File
@@ -609,35 +609,20 @@ Most workflows can be triggered manually via `workflow_dispatch` for testing.
### Native unit tests (C++)
Unit tests in `test/` directory with 17 test suites:
Unit tests in `test/` directory with 12 test suites:
- `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_mqtt/` - MQTT integration
- `test_radio/` - Radio interface
- `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
- `test_atak/` - ATAK integration
- `test_default/` - Default configuration
- `test_http_content_handler/` - HTTP handling
- `test_serial/` - Serial communication
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.
Run with: `pio test -e native`
Simulation testing: `bin/test-simulator.sh`
@@ -1,62 +0,0 @@
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,
});
}
+26 -105
View File
@@ -245,126 +245,47 @@ jobs:
path: ./*.elf
retention-days: 30
firmware-size-report:
shame:
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
- name: Download current manifests
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
uses: actions/download-artifact@v8
with:
path: ./manifests/
path: ./manifests-new/
pattern: manifest-*
merge-multiple: true
- name: Collect current firmware sizes
run: python3 bin/collect_sizes.py ./manifests/ ./current-sizes.json
- name: Upload size report artifact
- name: Upload combined manifests for later commit and global stats crunching.
uses: actions/upload-artifact@v7
id: upload-manifest
with:
name: firmware-sizes-${{ github.sha }}
name: manifests-${{ github.sha }}
overwrite: true
path: ./current-sizes.json
retention-days: 90
- name: Download baseline sizes from develop
path: manifests-new/*.mt.json
- name: Find the merge base
if: github.event_name == 'pull_request'
continue-on-error: true
id: baseline-develop
run: echo "MERGE_BASE=$(git merge-base "origin/$base" "$head")" >> $GITHUB_ENV
env:
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
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/
release-artifacts:
permissions: # Needed for 'gh release upload'.
+13 -13
View File
@@ -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) | `~/.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]` |
| Action | Command |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| Build a firmware variant | `pio run -e <env>` (e.g. `pio run -e rak4631`, `pio run -e heltec-v3`) |
| Build native macOS host binary | `pio run -e native-macos` (Homebrew prereqs + CH341 LoRa setup in `variants/native/portduino/platformio.ini`) |
| Clean + rebuild | `pio run -e <env> -t clean && pio run -e <env>` |
| Flash a device | `pio run -e <env> -t upload --upload-port <port>` (or use the `pio_flash` MCP tool) |
| Run firmware unit tests (native) | `pio test -e native` |
| Run MCP hardware tests | `./mcp-server/run-tests.sh` |
| Live TUI test runner | `mcp-server/.venv/bin/meshtastic-mcp-test-tui` |
| Format before commit | `trunk fmt` |
| Regenerate protobuf bindings | `bin/regen-protos.sh` |
| Generate CI matrix | `./bin/generate_ci_matrix.py all [--level pr]` |
## MCP server (device + test automation)
@@ -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 (17 suites; `pio test -e native`) |
| `test/` | Firmware unit tests (12 suites; `pio test -e native`) |
| `mcp-server/` | Python MCP server + pytest hardware integration tests |
| `mcp-server/tests/` | Tiered pytest suite: `unit/`, `mesh/`, `telemetry/`, `monitor/`, `recovery/`, `ui/`, `fleet/`, `admin/`, `provisioning/` |
| `.claude/commands/` | Claude Code slash command bodies |
+2 -2
View File
@@ -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 libjsoncpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \
&& 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 libjsoncpp26 libi2c0 libuv1t64 libusb-1.0-0-dev \
libc-bin libc6 libgpiod3 libyaml-cpp0.8 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
View File
@@ -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 libjsoncpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
+2 -2
View File
@@ -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 jsoncpp-dev bluez-dev \
libgpiod-dev yaml-cpp-dev bluez-dev \
libusb-dev i2c-tools-dev libuv-dev openssl-dev pkgconf argp-standalone \
libx11-dev libinput-dev libxkbcommon-dev sqlite-dev sdl2-dev \
&& 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 jsoncpp libusb \
shadow libstdc++ libbsd libgpiod yaml-cpp libusb \
i2c-tools libuv libx11 libinput libxkbcommon sdl2 \
&& rm -rf /var/cache/apk/* \
&& mkdir -p /var/lib/meshtasticd \
-53
View File
@@ -1,53 +0,0 @@
#!/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}")
+95
View File
@@ -0,0 +1,95 @@
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)
-171
View File
@@ -1,171 +0,0 @@
#!/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()
-262
View File
@@ -1,262 +0,0 @@
#!/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)
-32
View File
@@ -1,32 +0,0 @@
{
"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"
}
-54
View File
@@ -1,54 +0,0 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v6.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
["0x239A", "0x4405"],
["0x239A", "0x0029"],
["0x239A", "0x002A"],
["0x2886", "0x1667"]
],
"usb_product": "HT-n5262",
"mcu": "nrf52840",
"variant": "heltec_mesh_node_t1",
"variants_dir": "variants",
"bsp": {
"name": "adafruit"
},
"softdevice": {
"sd_flags": "-DS140",
"sd_name": "s140",
"sd_version": "6.1.1",
"sd_fwid": "0x00B6"
},
"bootloader": {
"settings_addr": "0xFF000"
}
},
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"onboard_tools": ["jlink"],
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52840-mdk-rs"
},
"frameworks": ["arduino"],
"name": "Heltec Mesh Node T1",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
"protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"],
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true
},
"url": "https://heltec.org",
"vendor": "Heltec"
}
-1
View File
@@ -14,7 +14,6 @@ Build-Depends: debhelper-compat (= 13),
g++,
pkg-config,
libyaml-cpp-dev,
libjsoncpp-dev,
libgpiod-dev,
libbluetooth-dev,
libusb-1.0-0-dev,
+2 -6
View File
@@ -191,12 +191,10 @@ 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 -q -r fE --report-log=tests/reportlog.jsonl
exec "$VENV_PY" -m pytest tests/unit -v --report-log=tests/reportlog.jsonl
fi
# Default pytest args when the user passed none. Power users can invoke
@@ -212,13 +210,11 @@ 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 \
-q -r fE --tb=short
-v --tb=short
fi
# UI tier requires opencv-python-headless (and ideally easyocr). If it's
-1
View File
@@ -34,7 +34,6 @@ 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)
+6 -4
View File
@@ -186,16 +186,12 @@ lib_deps =
https://github.com/sparkfun/SparkFun_MAX3010x_Sensor_Library/archive/refs/tags/v1.1.2.zip
# renovate: datasource=github-tags depName=SparkFun 9DoF IMU Breakout ICM 20948 packageName=sparkfun/SparkFun_ICM-20948_ArduinoLibrary
https://github.com/sparkfun/SparkFun_ICM-20948_ArduinoLibrary/archive/refs/tags/v1.3.2.zip
# renovate: datasource=github-tags depName=TDK InvenSense ICM42670P packageName=tdk-invn-oss/motion.arduino.ICM42670P
https://github.com/tdk-invn-oss/motion.arduino.ICM42670P/archive/refs/tags/1.0.8.zip
# renovate: datasource=github-tags depName=Adafruit LTR390 Library packageName=adafruit/Adafruit_LTR390
https://github.com/adafruit/Adafruit_LTR390/archive/refs/tags/1.1.2.zip
# renovate: datasource=github-tags depName=Adafruit PCT2075 packageName=adafruit/Adafruit_PCT2075
https://github.com/adafruit/Adafruit_PCT2075/archive/refs/tags/1.0.6.zip
# renovate: datasource=github-tags depName=DFRobot_BMM150 packageName=dfrobot/DFRobot_BMM150
https://github.com/DFRobot/DFRobot_BMM150/archive/refs/tags/V1.0.0.zip
# renovate: datasource=github-tags depName=SparkFun MMC5983MA Magnetometer packageName=sparkfun/SparkFun_MMC5983MA_Magnetometer_Arduino_Library
https://github.com/sparkfun/SparkFun_MMC5983MA_Magnetometer_Arduino_Library/archive/refs/tags/v1.1.4.zip
# renovate: datasource=github-tags depName=Adafruit_TSL2561 packageName=adafruit/Adafruit_TSL2561
https://github.com/adafruit/Adafruit_TSL2561/archive/refs/tags/1.1.3.zip
# renovate: datasource=github-tags depName=BH1750_WE packageName=wollewald/BH1750_WE
@@ -208,10 +204,16 @@ 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 -25
View File
@@ -14,28 +14,4 @@ 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};
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;
}
0x23, 0x44, 0xE6, 0x06, 0x49, 0x6E, 0x3D, 0x5A};
-5
View File
@@ -24,14 +24,9 @@ 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);
};
-6
View File
@@ -51,12 +51,6 @@ 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;
+1 -11
View File
@@ -1,16 +1,6 @@
// TODO refactor this out with better radio configuration system
#ifdef USE_RF95
#ifndef RF95_RESET
#define RF95_RESET LORA_RESET
#endif
#ifndef RF95_IRQ
#define RF95_IRQ LORA_DIO0 // on SX1262 version this is a no connect DIO0
#endif
#ifndef RF95_DIO1
#define RF95_IRQ LORA_DIO0 // on SX1262 version this is a no connect DIO0
#define RF95_DIO1 LORA_DIO1 // Note: not really used for RF95, but used for pure SX127x
#endif
#endif
+3 -3
View File
@@ -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 ARCH_PORTDUINO
#if ENABLE_JSON_LOGGING || 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 = bluetoothApi && bluetoothApi->isActive() && bluetoothApi->isConnected();
isBleConnected = nimbleBluetooth && nimbleBluetooth->isActive() && nimbleBluetooth->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
bluetoothApi->sendLog(buffer.get(), size);
nimbleBluetooth->sendLog(buffer.get(), size);
#elif defined(ARCH_NRF52)
nrf52Bluetooth->sendLog(buffer.get(), size);
#elif defined(ARCH_NRF54L15)
-762
View File
@@ -1,762 +0,0 @@
#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, &currentRssi);
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
-43
View File
@@ -1,43 +0,0 @@
#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;
};
-8
View File
@@ -31,11 +31,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#endif
#if __has_include("SensorRtcHelper.hpp")
#include "SensorRtcHelper.hpp"
// SensorLib defines isBitSet as a macro; undefine it here to avoid conflicts
// with the SparkFun MMC5983MA library, which has a class method of the same name.
#ifdef isBitSet
#undef isBitSet
#endif
#endif
/* Offer chance for variant-specific defines */
@@ -248,7 +243,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define QMI8658_ADDR 0x6B
#define QMC5883L_ADDR 0x0D
#define HMC5883L_ADDR 0x1E
#define MMC5983MA_ADDR 0x30
#define SHTC3_ADDR 0x70
#define LPS22HB_ADDR 0x5C
#define LPS22HB_ADDR_ALT 0x5D
@@ -298,8 +292,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define DA217_ADDR 0x26
#define BMI270_ADDR 0x68
#define BMI270_ADDR_ALT 0x69
#define ICM42607P_ADDR 0x68
#define ICM42607P_ADDR_ALT 0x69
// -----------------------------------------------------------------------------
// LED
+2 -9
View File
@@ -37,15 +37,8 @@ ScanI2C::FoundDevice ScanI2C::firstKeyboard() const
ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const
{
ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX,
ICM20948, QMA6100P, BMM150, BMI270, ICM42607P};
return firstOfOrNONE(11, types);
}
ScanI2C::FoundDevice ScanI2C::firstMagnetometer() const
{
ScanI2C::DeviceType types[] = {MMC5983MA};
return firstOfOrNONE(1, types);
ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX, ICM20948, QMA6100P, BMM150, BMI270};
return firstOfOrNONE(10, types);
}
ScanI2C::FoundDevice ScanI2C::firstAQI() const
-4
View File
@@ -41,7 +41,6 @@ class ScanI2C
QMI8658,
QMC5883L,
HMC5883L,
MMC5983MA,
PMSA003I,
QMA6100P,
MPU6050,
@@ -66,7 +65,6 @@ class ScanI2C
FT6336U,
STK8BAXX,
ICM20948,
ICM42607P,
SCD4X,
MAX30102,
TPS65233,
@@ -151,8 +149,6 @@ class ScanI2C
FoundDevice firstAccelerometer() const;
FoundDevice firstMagnetometer() const;
FoundDevice firstAQI() const;
FoundDevice firstRGBLED() const;
+5 -20
View File
@@ -360,6 +360,9 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
SCAN_SIMPLE_CASE(ST7567_ADDRESS, SCREEN_ST7567, "ST7567", (uint8_t)addr.address);
#ifdef HAS_NCP5623
SCAN_SIMPLE_CASE(NCP5623_ADDR, NCP5623, "NCP5623", (uint8_t)addr.address);
#endif
#ifdef HAS_LP5562
SCAN_SIMPLE_CASE(LP5562_ADDR, LP5562, "LP5562", (uint8_t)addr.address);
#endif
case XPOWERS_AXP192_AXP2101_ADDRESS:
// Do we have the axp2101/192 or the TCA8418
@@ -597,18 +600,6 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
#else
SCAN_SIMPLE_CASE(PMSA003I_ADDR, PMSA003I, "PMSA003I", (uint8_t)addr.address)
#endif
case MMC5983MA_ADDR:
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x2F), 1);
if (registerValue == 0x30) {
type = MMC5983MA;
logFoundDevice("MMC5983MA", (uint8_t)addr.address);
#ifdef HAS_LP5562
} else {
type = LP5562;
logFoundDevice("LP5562", (uint8_t)addr.address);
#endif
}
break;
case BMA423_ADDR: // this can also be LIS3DH_ADDR_ALT
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 2);
if (registerValue == 0x3300 || registerValue == 0x3333) { // RAK4631 WisBlock has LIS3DH register at 0x3333
@@ -763,8 +754,8 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
}
break;
case ICM20948_ADDR: // same as BMX160_ADDR, BMI270_ADDR_ALT, ICM42607P_ADDR_ALT, and SEN5X_ADDR
case ICM20948_ADDR_ALT: // same as MPU6050_ADDR, BMI270_ADDR, and ICM42607P_ADDR
case ICM20948_ADDR: // same as BMX160_ADDR, BMI270_ADDR_ALT, and SEN5X_ADDR
case ICM20948_ADDR_ALT: // same as MPU6050_ADDR, BMI270_ADDR
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1);
#ifdef HAS_ICM20948
type = ICM20948;
@@ -784,12 +775,6 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
logFoundDevice("BMX160", (uint8_t)addr.address);
break;
} else {
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x75), 1); // WHO_AM_I
if (registerValue == 0x60) {
type = ICM42607P;
logFoundDevice("ICM-42607-P", (uint8_t)addr.address);
break;
}
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
String prod = "";
prod = readSEN5xProductName(i2cBus, addr.address);
+457 -237
View File
@@ -505,12 +505,15 @@ bool GPS::setup()
int msglen = 0;
if (tx_gpio && gnssModel == GNSS_MODEL_UNKNOWN) {
if (probeTries < GPS_PROBETRIES) {
bootString = "Probing GPS...";
gnssModel = probe(serialSpeeds[speedSelect]);
if (gnssModel == GNSS_MODEL_UNKNOWN) {
if (currentStep == 0 && ++speedSelect == array_count(serialSpeeds)) {
speedSelect = 0;
++probeTries;
}
} else {
currentStep = 0;
}
}
// Rare Serial Speeds
@@ -522,6 +525,8 @@ bool GPS::setup()
LOG_WARN("Give up on GPS probe and set to %d", GPS_BAUDRATE);
return true;
}
} else {
currentStep = 0;
}
}
#endif
@@ -538,86 +543,133 @@ bool GPS::setup()
* t-beam-s3-core uses the same L76K GNSS module as t-echo.
* Unlike t-echo, L76K uses 9600 baud rate for communication by default.
* */
// Initialize the L76K Chip, use GPS + GLONASS + BEIDOU
_serial_gps->write("$PCAS04,7*1E\r\n");
delay(250);
// only ask for RMC and GGA
_serial_gps->write("$PCAS03,1,0,0,0,1,0,0,0,0,0,,,0,0*02\r\n");
delay(250);
// Switch to Vehicle Mode, since SoftRF enables Aviation < 2g
_serial_gps->write("$PCAS11,3*1E\r\n");
delay(250);
if (currentStep == 0) {
// Initialize the L76K Chip, use GPS + GLONASS + BEIDOU
_serial_gps->write("$PCAS04,7*1E\r\n");
currentStep++;
currentDelay = 250;
return false;
} else if (currentStep == 1) {
// only ask for RMC and GGA
_serial_gps->write("$PCAS03,1,0,0,0,1,0,0,0,0,0,,,0,0*02\r\n");
currentStep++;
currentDelay = 250;
return false;
} else if (currentStep == 2) {
// Switch to Vehicle Mode, since SoftRF enables Aviation < 2g
_serial_gps->write("$PCAS11,3*1E\r\n");
currentDelay = 250;
}
} else if (gnssModel == GNSS_MODEL_MTK_L76B) {
// Waveshare Pico-GPS hat uses the L76B with 9600 baud
// Initialize the L76B Chip, use GPS + GLONASS
// See note in L76_Series_GNSS_Protocol_Specification, chapter 3.29
_serial_gps->write("$PMTK353,1,1,0,0,0*2B\r\n");
// Above command will reset the GPS and takes longer before it will accept new commands
delay(1000);
// only ask for RMC and GGA (GNRMC and GNGGA)
// See note in L76_Series_GNSS_Protocol_Specification, chapter 2.1
_serial_gps->write("$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n");
delay(250);
// Enable SBAS
_serial_gps->write("$PMTK301,2*2E\r\n");
delay(250);
// Enable PPS for 2D/3D fix only
_serial_gps->write("$PMTK285,3,100*3F\r\n");
delay(250);
// Switch to Fitness Mode, for running and walking purpose with low speed (<5 m/s)
_serial_gps->write("$PMTK886,1*29\r\n");
delay(250);
if (currentStep == 0) {
_serial_gps->write("$PMTK353,1,1,0,0,0*2B\r\n");
currentStep++;
currentDelay = 1000;
return false;
} else if (currentStep == 1) {
// Above command will reset the GPS and takes longer before it will accept new commands
// only ask for RMC and GGA (GNRMC and GNGGA)
// See note in L76_Series_GNSS_Protocol_Specification, chapter 2.1
_serial_gps->write("$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n");
currentStep++;
currentDelay = 250;
return false;
} else if (currentStep == 2) {
// Enable SBAS
_serial_gps->write("$PMTK301,2*2E\r\n");
currentStep++;
currentDelay = 250;
return false;
} else if (currentStep == 3) {
// Enable PPS for 2D/3D fix only
_serial_gps->write("$PMTK285,3,100*3F\r\n");
currentStep++;
currentDelay = 250;
return false;
} else if (currentStep == 4) {
// Switch to Fitness Mode, for running and walking purpose with low speed (<5 m/s)
_serial_gps->write("$PMTK886,1*29\r\n");
currentDelay = 250;
}
} else if (gnssModel == GNSS_MODEL_MTK_PA1010D) {
// PA1010D is used in the Pimoroni GPS board.
// Enable all constellations.
_serial_gps->write("$PMTK353,1,1,1,1,1*2A\r\n");
// Above command will reset the GPS and takes longer before it will accept new commands
delay(1000);
// Only ask for RMC and GGA (GNRMC and GNGGA)
_serial_gps->write("$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n");
delay(250);
// Enable SBAS / WAAS
_serial_gps->write("$PMTK301,2*2E\r\n");
delay(250);
if (currentStep == 0) {
// Enable all constellations.
_serial_gps->write("$PMTK353,1,1,1,1,1*2A\r\n");
currentStep++;
currentDelay = 1000;
return false;
} else if (currentStep == 1) {
// Above command will reset the GPS and takes longer before it will accept new commands
// Only ask for RMC and GGA (GNRMC and GNGGA)
_serial_gps->write("$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n");
currentStep++;
currentDelay = 250;
return false;
} else if (currentStep == 2) {
// Enable SBAS / WAAS
_serial_gps->write("$PMTK301,2*2E\r\n");
currentDelay = 250;
}
} else if (gnssModel == GNSS_MODEL_MTK_PA1616S) {
// PA1616S is used in some GPS breakout boards from Adafruit
// PA1616S does not have GLONASS capability. PA1616D does, but is not implemented here.
_serial_gps->write("$PMTK353,1,0,0,0,0*2A\r\n");
// Above command will reset the GPS and takes longer before it will accept new commands
delay(1000);
// Only ask for RMC and GGA (GNRMC and GNGGA)
_serial_gps->write("$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n");
delay(250);
// Enable SBAS / WAAS
_serial_gps->write("$PMTK301,2*2E\r\n");
delay(250);
if (currentStep == 0) {
_serial_gps->write("$PMTK353,1,0,0,0,0*2A\r\n");
currentStep++;
currentDelay = 1000;
return false;
} else if (currentStep == 1) {
// Above command will reset the GPS and takes longer before it will accept new commands
// Only ask for RMC and GGA (GNRMC and GNGGA)
_serial_gps->write("$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n");
currentStep++;
currentDelay = 250;
return false;
} else if (currentStep == 2) {
// Enable SBAS / WAAS
_serial_gps->write("$PMTK301,2*2E\r\n");
currentDelay = 250;
}
} else if (gnssModel == GNSS_MODEL_ATGM336H) {
// Set the initial configuration of the device - these _should_ work for most AT6558 devices
msglen = makeCASPacket(0x06, 0x07, sizeof(_message_CAS_CFG_NAVX_CONF), _message_CAS_CFG_NAVX_CONF);
_serial_gps->write(UBXscratch, msglen);
if (getACKCas(0x06, 0x07, 250) != GNSS_RESPONSE_OK) {
LOG_WARN("ATGM336H: Could not set Config");
}
// Set the update frequency to 1Hz
msglen = makeCASPacket(0x06, 0x04, sizeof(_message_CAS_CFG_RATE_1HZ), _message_CAS_CFG_RATE_1HZ);
_serial_gps->write(UBXscratch, msglen);
if (getACKCas(0x06, 0x04, 250) != GNSS_RESPONSE_OK) {
LOG_WARN("ATGM336H: Could not set Update Frequency");
}
// Set the NEMA output messages
// Ask for only RMC and GGA
uint8_t fields[] = {CAS_NEMA_RMC, CAS_NEMA_GGA};
for (unsigned int i = 0; i < sizeof(fields); i++) {
if (currentStep == 0) {
// Set the initial configuration of the device - these _should_ work for most AT6558 devices
msglen = makeCASPacket(0x06, 0x07, sizeof(_message_CAS_CFG_NAVX_CONF), _message_CAS_CFG_NAVX_CONF);
_serial_gps->write(UBXscratch, msglen);
if (getACKCas(0x06, 0x07, 250) != GNSS_RESPONSE_OK) {
LOG_WARN("ATGM336H: Could not set Config");
}
currentStep++;
return false;
} else if (currentStep == 1) {
// Set the update frequency to 1Hz
msglen = makeCASPacket(0x06, 0x04, sizeof(_message_CAS_CFG_RATE_1HZ), _message_CAS_CFG_RATE_1HZ);
_serial_gps->write(UBXscratch, msglen);
if (getACKCas(0x06, 0x04, 250) != GNSS_RESPONSE_OK) {
LOG_WARN("ATGM336H: Could not set Update Frequency");
}
currentStep++;
return false;
} else if (currentStep == 2) {
// Set the NEMA output messages - Ask for only RMC and GGA
// Construct a CAS-CFG-MSG packet
uint8_t cas_cfg_msg_packet[] = {0x4e, fields[i], 0x01, 0x00};
uint8_t cas_cfg_msg_packet[] = {0x4e, CAS_NEMA_RMC, 0x01, 0x00};
msglen = makeCASPacket(0x06, 0x01, sizeof(cas_cfg_msg_packet), cas_cfg_msg_packet);
_serial_gps->write(UBXscratch, msglen);
if (getACKCas(0x06, 0x01, 250) != GNSS_RESPONSE_OK) {
LOG_WARN("ATGM336H: Could not enable NMEA MSG: %d", fields[i]);
LOG_WARN("ATGM336H: Could not enable NMEA MSG: %d", CAS_NEMA_RMC);
}
currentStep++;
return false;
} else if (currentStep == 3) {
uint8_t cas_cfg_msg_packet[] = {0x4e, CAS_NEMA_GGA, 0x01, 0x00};
msglen = makeCASPacket(0x06, 0x01, sizeof(cas_cfg_msg_packet), cas_cfg_msg_packet);
_serial_gps->write(UBXscratch, msglen);
if (getACKCas(0x06, 0x01, 250) != GNSS_RESPONSE_OK) {
LOG_WARN("ATGM336H: Could not enable NMEA MSG: %d", CAS_NEMA_GGA);
}
}
} else if (gnssModel == GNSS_MODEL_UC6580) {
@@ -625,195 +677,363 @@ bool GPS::setup()
// use GPS L1 & L5 + BDS B1I & B2a + GLONASS L1 + GALILEO E1 & E5a + SBAS + QZSS
// This will reset the receiver, so wait a bit afterwards
// The paranoid will wait for the OK*04 confirmation response after each command.
_serial_gps->write("$CFGSYS,h35155\r\n");
delay(750);
// Must be done after the CFGSYS command
// Turn off GSV messages, we don't really care about which and where the sats are, maybe someday.
_serial_gps->write("$CFGMSG,0,3,0\r\n");
delay(250);
// Turn off GSA messages, TinyGPS++ doesn't use this message.
_serial_gps->write("$CFGMSG,0,2,0\r\n");
delay(250);
// Turn off NOTICE __TXT messages, these may provide Unicore some info but we don't care.
_serial_gps->write("$CFGMSG,6,0,0\r\n");
delay(250);
_serial_gps->write("$CFGMSG,6,1,0\r\n");
delay(250);
} else if (IS_ONE_OF(gnssModel, GNSS_MODEL_AG3335, GNSS_MODEL_AG3352)) {
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_IN ||
config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_NP_865) {
_serial_gps->write("$PAIR066,1,0,1,0,0,1*3B\r\n"); // Enable GPS+GALILEO+NAVIC
// GPS GLONASS GALILEO BDS QZSS NAVIC
// 1 0 1 0 0 1
} else {
_serial_gps->write("$PAIR066,1,1,1,1,0,0*3A\r\n"); // Enable GPS+GLONASS+GALILEO+BDS
// GPS GLONASS GALILEO BDS QZSS NAVIC
// 1 1 1 1 0 0
if (currentStep == 0) {
_serial_gps->write("$CFGSYS,h35155\r\n");
currentStep++;
currentDelay = 750;
return false;
} else if (currentStep == 1) {
// Must be done after the CFGSYS command
// Turn off GSV messages, we don't really care about which and where the sats are, maybe someday.
_serial_gps->write("$CFGMSG,0,3,0\r\n");
currentStep++;
currentDelay = 250;
return false;
} else if (currentStep == 2) {
// Turn off GSA messages, TinyGPS++ doesn't use this message.
_serial_gps->write("$CFGMSG,0,2,0\r\n");
currentStep++;
currentDelay = 250;
return false;
} else if (currentStep == 3) {
// Turn off NOTICE __TXT messages, these may provide Unicore some info but we don't care.
_serial_gps->write("$CFGMSG,6,0,0\r\n");
currentStep++;
currentDelay = 250;
return false;
} else if (currentStep == 4) {
_serial_gps->write("$CFGMSG,6,1,0\r\n");
currentDelay = 250;
}
} else if (IS_ONE_OF(gnssModel, GNSS_MODEL_AG3335, GNSS_MODEL_AG3352)) {
if (currentStep == 0) {
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_IN ||
config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_NP_865) {
_serial_gps->write("$PAIR066,1,0,1,0,0,1*3B\r\n"); // Enable GPS+GALILEO+NAVIC
// GPS GLONASS GALILEO BDS QZSS NAVIC
// 1 0 1 0 0 1
} else {
_serial_gps->write("$PAIR066,1,1,1,1,0,0*3A\r\n"); // Enable GPS+GLONASS+GALILEO+BDS
// GPS GLONASS GALILEO BDS QZSS NAVIC
// 1 1 1 1 0 0
}
currentStep++;
return false;
} else if (currentStep == 1) {
// Configure NMEA (sentences will output once per fix)
_serial_gps->write("$PAIR062,0,1*3F\r\n"); // GGA ON
currentStep++;
return false;
} else if (currentStep == 2) {
_serial_gps->write("$PAIR062,1,0*3F\r\n"); // GLL OFF
currentStep++;
return false;
} else if (currentStep == 3) {
_serial_gps->write("$PAIR062,2,0*3C\r\n"); // GSA OFF
currentStep++;
return false;
} else if (currentStep == 4) {
_serial_gps->write("$PAIR062,3,0*3D\r\n"); // GSV OFF
currentStep++;
return false;
} else if (currentStep == 5) {
_serial_gps->write("$PAIR062,4,1*3B\r\n"); // RMC ON
currentStep++;
return false;
} else if (currentStep == 6) {
_serial_gps->write("$PAIR062,5,0*3B\r\n"); // VTG OFF
currentStep++;
return false;
} else if (currentStep == 7) {
_serial_gps->write("$PAIR062,6,0*38\r\n"); // ZDA ON
currentStep++;
currentDelay = 250;
return false;
} else if (currentStep == 8) {
_serial_gps->write("$PAIR513*3D\r\n"); // save configuration
}
// Configure NMEA (sentences will output once per fix)
_serial_gps->write("$PAIR062,0,1*3F\r\n"); // GGA ON
_serial_gps->write("$PAIR062,1,0*3F\r\n"); // GLL OFF
_serial_gps->write("$PAIR062,2,0*3C\r\n"); // GSA OFF
_serial_gps->write("$PAIR062,3,0*3D\r\n"); // GSV OFF
_serial_gps->write("$PAIR062,4,1*3B\r\n"); // RMC ON
_serial_gps->write("$PAIR062,5,0*3B\r\n"); // VTG OFF
_serial_gps->write("$PAIR062,6,0*38\r\n"); // ZDA ON
delay(250);
_serial_gps->write("$PAIR513*3D\r\n"); // save configuration
} else if (gnssModel == GNSS_MODEL_UBLOX6) {
clearBuffer();
SEND_UBX_PACKET(0x06, 0x02, _message_DISABLE_TXT_INFO, "disable text info messages", 500);
SEND_UBX_PACKET(0x06, 0x39, _message_JAM_6_7, "enable interference resistance", 500);
SEND_UBX_PACKET(0x06, 0x23, _message_NAVX5, "configure NAVX5 settings", 500);
// Turn off unwanted NMEA messages, set update rate
SEND_UBX_PACKET(0x06, 0x08, _message_1HZ, "set GPS update rate", 500);
SEND_UBX_PACKET(0x06, 0x01, _message_GLL, "disable NMEA GLL", 500);
SEND_UBX_PACKET(0x06, 0x01, _message_GSA, "enable NMEA GSA", 500);
SEND_UBX_PACKET(0x06, 0x01, _message_GSV, "disable NMEA GSV", 500);
SEND_UBX_PACKET(0x06, 0x01, _message_VTG, "disable NMEA VTG", 500);
SEND_UBX_PACKET(0x06, 0x01, _message_RMC, "enable NMEA RMC", 500);
SEND_UBX_PACKET(0x06, 0x01, _message_GGA, "enable NMEA GGA", 500);
clearBuffer();
SEND_UBX_PACKET(0x06, 0x11, _message_CFG_RXM_ECO, "enable powersave ECO mode for Neo-6", 500);
SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500);
SEND_UBX_PACKET(0x06, 0x01, _message_AID, "disable UBX-AID", 500);
msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE), _message_SAVE);
_serial_gps->write(UBXscratch, msglen);
if (getACK(0x06, 0x09, 2000) != GNSS_RESPONSE_OK) {
LOG_WARN("Unable to save GNSS module config");
} else {
LOG_INFO("GNSS module config saved!");
if (currentStep == 0) {
clearBuffer();
SEND_UBX_PACKET(0x06, 0x02, _message_DISABLE_TXT_INFO, "disable text info messages", 500);
currentStep++;
return false;
} else if (currentStep == 1) {
SEND_UBX_PACKET(0x06, 0x39, _message_JAM_6_7, "enable interference resistance", 500);
currentStep++;
return false;
} else if (currentStep == 2) {
SEND_UBX_PACKET(0x06, 0x23, _message_NAVX5, "configure NAVX5 settings", 500);
currentStep++;
return false;
} else if (currentStep == 3) {
// Turn off unwanted NMEA messages, set update rate
SEND_UBX_PACKET(0x06, 0x08, _message_1HZ, "set GPS update rate", 500);
currentStep++;
return false;
} else if (currentStep == 4) {
SEND_UBX_PACKET(0x06, 0x01, _message_GLL, "disable NMEA GLL", 500);
currentStep++;
return false;
} else if (currentStep == 5) {
SEND_UBX_PACKET(0x06, 0x01, _message_GSA, "enable NMEA GSA", 500);
currentStep++;
return false;
} else if (currentStep == 6) {
SEND_UBX_PACKET(0x06, 0x01, _message_GSV, "disable NMEA GSV", 500);
currentStep++;
return false;
} else if (currentStep == 7) {
SEND_UBX_PACKET(0x06, 0x01, _message_VTG, "disable NMEA VTG", 500);
currentStep++;
return false;
} else if (currentStep == 8) {
SEND_UBX_PACKET(0x06, 0x01, _message_RMC, "enable NMEA RMC", 500);
currentStep++;
return false;
} else if (currentStep == 9) {
SEND_UBX_PACKET(0x06, 0x01, _message_GGA, "enable NMEA GGA", 500);
currentStep++;
return false;
} else if (currentStep == 10) {
clearBuffer();
SEND_UBX_PACKET(0x06, 0x11, _message_CFG_RXM_ECO, "enable powersave ECO mode for Neo-6", 500);
currentStep++;
return false;
} else if (currentStep == 11) {
SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500);
currentStep++;
return false;
} else if (currentStep == 12) {
SEND_UBX_PACKET(0x06, 0x01, _message_AID, "disable UBX-AID", 500);
currentStep++;
return false;
} else if (currentStep == 13) {
msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE), _message_SAVE);
_serial_gps->write(UBXscratch, msglen);
if (getACK(0x06, 0x09, 2000) != GNSS_RESPONSE_OK) {
LOG_WARN("Unable to save GNSS module config");
} else {
LOG_INFO("GNSS module config saved!");
}
}
} else if (IS_ONE_OF(gnssModel, GNSS_MODEL_UBLOX7, GNSS_MODEL_UBLOX8, GNSS_MODEL_UBLOX9)) {
if (gnssModel == GNSS_MODEL_UBLOX7) {
LOG_DEBUG("Set GPS+SBAS");
msglen = makeUBXPacket(0x06, 0x3e, sizeof(_message_GNSS_7), _message_GNSS_7);
_serial_gps->write(UBXscratch, msglen);
} else { // 8,9
msglen = makeUBXPacket(0x06, 0x3e, sizeof(_message_GNSS_8), _message_GNSS_8);
_serial_gps->write(UBXscratch, msglen);
}
if (getACK(0x06, 0x3e, 800) == GNSS_RESPONSE_NAK) {
// It's not critical if the module doesn't acknowledge this configuration.
LOG_DEBUG("reconfigure GNSS - defaults maintained. Is this module GPS-only?");
} else {
if (currentStep == 0) {
if (gnssModel == GNSS_MODEL_UBLOX7) {
LOG_INFO("GPS+SBAS configured");
LOG_DEBUG("Set GPS+SBAS");
msglen = makeUBXPacket(0x06, 0x3e, sizeof(_message_GNSS_7), _message_GNSS_7);
_serial_gps->write(UBXscratch, msglen);
} else { // 8,9
LOG_INFO("GPS+SBAS+GLONASS+Galileo configured");
msglen = makeUBXPacket(0x06, 0x3e, sizeof(_message_GNSS_8), _message_GNSS_8);
_serial_gps->write(UBXscratch, msglen);
}
// Documentation say, we need wait at least 0.5s after reconfiguration of GNSS module, before sending next
// commands for the M8 it tends to be more... 1 sec should be enough ;>)
delay(1000);
}
// Disable Text Info messages //6,7,8,9
clearBuffer();
SEND_UBX_PACKET(0x06, 0x02, _message_DISABLE_TXT_INFO, "disable text info messages", 500);
if (gnssModel == GNSS_MODEL_UBLOX8) { // 8
if (getACK(0x06, 0x3e, 800) == GNSS_RESPONSE_NAK) {
// It's not critical if the module doesn't acknowledge this configuration.
LOG_DEBUG("reconfigure GNSS - defaults maintained. Is this module GPS-only?");
} else {
if (gnssModel == GNSS_MODEL_UBLOX7) {
LOG_INFO("GPS+SBAS configured");
} else { // 8,9
LOG_INFO("GPS+SBAS+GLONASS+Galileo configured");
}
// Documentation say, we need wait at least 0.5s after reconfiguration of GNSS module, before sending next
// commands for the M8 it tends to be more... 1 sec should be enough ;>)
delay(1000);
}
currentStep++;
return false;
} else if (currentStep == 1) {
// Disable Text Info messages //6,7,8,9
clearBuffer();
SEND_UBX_PACKET(0x06, 0x39, _message_JAM_8, "enable interference resistance", 500);
clearBuffer();
SEND_UBX_PACKET(0x06, 0x23, _message_NAVX5_8, "configure NAVX5_8 settings", 500);
} else { // 6,7,9
SEND_UBX_PACKET(0x06, 0x39, _message_JAM_6_7, "enable interference resistance", 500);
SEND_UBX_PACKET(0x06, 0x23, _message_NAVX5, "configure NAVX5 settings", 500);
}
// Turn off unwanted NMEA messages, set update rate
SEND_UBX_PACKET(0x06, 0x08, _message_1HZ, "set GPS update rate", 500);
SEND_UBX_PACKET(0x06, 0x01, _message_GLL, "disable NMEA GLL", 500);
SEND_UBX_PACKET(0x06, 0x01, _message_GSA, "enable NMEA GSA", 500);
SEND_UBX_PACKET(0x06, 0x01, _message_GSV, "disable NMEA GSV", 500);
SEND_UBX_PACKET(0x06, 0x01, _message_VTG, "disable NMEA VTG", 500);
SEND_UBX_PACKET(0x06, 0x01, _message_RMC, "enable NMEA RMC", 500);
SEND_UBX_PACKET(0x06, 0x01, _message_GGA, "enable NMEA GGA", 500);
if (ublox_info.protocol_version >= 18) {
clearBuffer();
SEND_UBX_PACKET(0x06, 0x86, _message_PMS, "enable powersave for GPS", 500);
SEND_UBX_PACKET(0x06, 0x02, _message_DISABLE_TXT_INFO, "disable text info messages", 500);
currentStep++;
return false;
} else if (currentStep == 2) {
if (gnssModel == GNSS_MODEL_UBLOX8) { // 8
clearBuffer();
SEND_UBX_PACKET(0x06, 0x39, _message_JAM_8, "enable interference resistance", 500);
} else { // 7,9
SEND_UBX_PACKET(0x06, 0x39, _message_JAM_6_7, "enable interference resistance", 500);
}
currentStep++;
return false;
} else if (currentStep == 3) {
if (gnssModel == GNSS_MODEL_UBLOX8) { // 8
clearBuffer();
SEND_UBX_PACKET(0x06, 0x23, _message_NAVX5_8, "configure NAVX5_8 settings", 500);
} else { // 7,9
SEND_UBX_PACKET(0x06, 0x23, _message_NAVX5, "configure NAVX5 settings", 500);
}
currentStep++;
return false;
} else if (currentStep == 4) {
// Turn off unwanted NMEA messages, set update rate
SEND_UBX_PACKET(0x06, 0x08, _message_1HZ, "set GPS update rate", 500);
currentStep++;
return false;
} else if (currentStep == 5) {
SEND_UBX_PACKET(0x06, 0x01, _message_GLL, "disable NMEA GLL", 500);
currentStep++;
return false;
} else if (currentStep == 6) {
SEND_UBX_PACKET(0x06, 0x01, _message_GSA, "enable NMEA GSA", 500);
currentStep++;
return false;
} else if (currentStep == 7) {
SEND_UBX_PACKET(0x06, 0x01, _message_GSV, "disable NMEA GSV", 500);
currentStep++;
return false;
} else if (currentStep == 8) {
SEND_UBX_PACKET(0x06, 0x01, _message_VTG, "disable NMEA VTG", 500);
currentStep++;
return false;
} else if (currentStep == 9) {
SEND_UBX_PACKET(0x06, 0x01, _message_RMC, "enable NMEA RMC", 500);
currentStep++;
return false;
} else if (currentStep == 10) {
SEND_UBX_PACKET(0x06, 0x01, _message_GGA, "enable NMEA GGA", 500);
currentStep++;
return false;
} else if (currentStep == 11) {
if (ublox_info.protocol_version >= 18) {
clearBuffer();
SEND_UBX_PACKET(0x06, 0x86, _message_PMS, "enable powersave for GPS", 500);
} else {
SEND_UBX_PACKET(0x06, 0x11, _message_CFG_RXM_PSM, "enable powersave mode for GPS", 500);
}
currentStep++;
return false;
} else if (currentStep == 12) {
SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500);
currentStep++;
return false;
} else if (currentStep == 13) {
// For M8 we want to enable NMEA version 4.10 so we can see the additional sats.
if (gnssModel == GNSS_MODEL_UBLOX8) {
if (ublox_info.protocol_version >= 18 && gnssModel == GNSS_MODEL_UBLOX8) {
clearBuffer();
SEND_UBX_PACKET(0x06, 0x17, _message_NMEA, "enable NMEA 4.10", 500);
currentStep++;
return false;
}
msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE), _message_SAVE);
_serial_gps->write(UBXscratch, msglen);
if (getACK(0x06, 0x09, 2000) != GNSS_RESPONSE_OK) {
LOG_WARN("Unable to save GNSS module config");
} else {
LOG_INFO("GNSS module configuration saved!");
}
} else if (currentStep == 14) {
msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE), _message_SAVE);
_serial_gps->write(UBXscratch, msglen);
if (getACK(0x06, 0x09, 2000) != GNSS_RESPONSE_OK) {
LOG_WARN("Unable to save GNSS module config");
} else {
LOG_INFO("GNSS module configuration saved!");
}
} else {
SEND_UBX_PACKET(0x06, 0x11, _message_CFG_RXM_PSM, "enable powersave mode for GPS", 500);
SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500);
}
msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE), _message_SAVE);
_serial_gps->write(UBXscratch, msglen);
if (getACK(0x06, 0x09, 2000) != GNSS_RESPONSE_OK) {
LOG_WARN("Unable to save GNSS module config");
} else {
LOG_INFO("GNSS module configuration saved!");
}
} else if (gnssModel == GNSS_MODEL_UBLOX10) {
delay(1000);
clearBuffer();
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_NMEA_RAM, "disable NMEA messages in M10 RAM", 300);
delay(750);
clearBuffer();
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_NMEA_BBR, "disable NMEA messages in M10 BBR", 300);
delay(750);
clearBuffer();
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_TXT_INFO_RAM, "disable Info messages for M10 GPS RAM", 300);
delay(750);
// Next disable Info txt messages in BBR layer
clearBuffer();
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_TXT_INFO_BBR, "disable Info messages for M10 GPS BBR", 300);
delay(750);
// Do M10 configuration for Power Management.
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_PM_RAM, "enable powersave for M10 GPS RAM", 300);
delay(750);
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_PM_BBR, "enable powersave for M10 GPS BBR", 300);
delay(750);
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ITFM_RAM, "enable jam detection M10 GPS RAM", 300);
delay(750);
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ITFM_BBR, "enable jam detection M10 GPS BBR", 300);
delay(750);
// Here is where the init commands should go to do further M10 initialization.
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_SBAS_RAM, "disable SBAS M10 GPS RAM", 300);
delay(750); // will cause a receiver restart so wait a bit
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_SBAS_BBR, "disable SBAS M10 GPS BBR", 300);
delay(750); // will cause a receiver restart so wait a bit
// Done with initialization, Now enable wanted NMEA messages in BBR layer so they will survive a periodic
// sleep.
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ENABLE_NMEA_BBR, "enable messages for M10 GPS BBR", 300);
delay(750);
// Next enable wanted NMEA messages in RAM layer
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ENABLE_NMEA_RAM, "enable messages for M10 GPS RAM", 500);
delay(750);
// As the M10 has no flash, the best we can do to preserve the config is to set it in RAM and BBR.
// BBR will survive a restart, and power off for a while, but modules with small backup
// batteries or super caps will not retain the config for a long power off time.
msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE_10), _message_SAVE_10);
_serial_gps->write(UBXscratch, msglen);
if (getACK(0x06, 0x09, 2000) != GNSS_RESPONSE_OK) {
LOG_WARN("Unable to save GNSS module config");
} else {
LOG_INFO("GNSS module configuration saved!");
if (currentStep == 0) {
LOG_INFO("Configuring M10 GPS step 0");
delay(1000);
clearBuffer();
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_NMEA_RAM, "disable NMEA messages in M10 RAM", 300);
currentStep++;
currentDelay = 750;
return false;
} else if (currentStep == 1) {
LOG_INFO("Configuring M10 GPS step 1");
clearBuffer();
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_NMEA_BBR, "disable NMEA messages in M10 BBR", 300);
currentStep++;
currentDelay = 750;
return false;
} else if (currentStep == 2) {
clearBuffer();
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_TXT_INFO_RAM, "disable Info messages for M10 GPS RAM", 300);
currentStep++;
currentDelay = 750;
return false;
} else if (currentStep == 3) {
// Next disable Info txt messages in BBR layer
clearBuffer();
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_TXT_INFO_BBR, "disable Info messages for M10 GPS BBR", 300);
currentStep++;
currentDelay = 750;
return false;
} else if (currentStep == 4) {
// Do M10 configuration for Power Management.
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_PM_RAM, "enable powersave for M10 GPS RAM", 300);
currentStep++;
currentDelay = 750;
return false;
} else if (currentStep == 5) {
// Next enable powersave in BBR layer
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_PM_BBR, "enable powersave for M10 GPS BBR", 300);
currentStep++;
currentDelay = 750;
return false;
} else if (currentStep == 6) {
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ITFM_RAM, "enable jam detection M10 GPS RAM", 300);
currentStep++;
currentDelay = 750;
return false;
} else if (currentStep == 7) {
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ITFM_BBR, "enable jam detection M10 GPS BBR", 300);
currentStep++;
currentDelay = 750;
return false;
} else if (currentStep == 8) {
// Here is where the init commands should go to do further M10 initialization.
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_SBAS_RAM, "disable SBAS M10 GPS RAM", 300);
currentStep++;
currentDelay = 750;
return false;
} else if (currentStep == 9) {
// will cause a receiver restart so wait a bit
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_SBAS_BBR, "disable SBAS M10 GPS BBR", 300);
currentStep++;
currentDelay = 750;
return false;
} else if (currentStep == 10) {
// Done with initialization, Now enable wanted NMEA messages in BBR layer so they will survive a periodic
// sleep.
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ENABLE_NMEA_BBR, "enable messages for M10 GPS BBR", 300);
currentStep++;
currentDelay = 750;
return false;
} else if (currentStep == 11) {
// Next enable wanted NMEA messages in RAM layer
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ENABLE_NMEA_RAM, "enable messages for M10 GPS RAM", 500);
currentStep++;
currentDelay = 750;
return false;
} else if (currentStep == 12) {
// As the M10 has no flash, the best we can do to preserve the config is to set it in RAM and BBR.
// BBR will survive a restart, and power off for a while, but modules with small backup
// batteries or super caps will not retain the config for a long power off time.
msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE_10), _message_SAVE_10);
_serial_gps->write(UBXscratch, msglen);
if (getACK(0x06, 0x09, 2000) != GNSS_RESPONSE_OK) {
LOG_WARN("Unable to save GNSS module config");
} else {
LOG_INFO("GNSS module configuration saved!");
}
didSerialInit = true;
}
} else if (gnssModel == GNSS_MODEL_CM121) {
// only ask for RMC and GGA
// enable GGA
_serial_gps->write("$CFGMSG,0,0,1,1*1B\r\n");
delay(250);
// enable RMC
_serial_gps->write("$CFGMSG,0,4,1,1*1F\r\n");
delay(250);
if (currentStep == 0) {
// enable GGA
_serial_gps->write("$CFGMSG,0,0,1,1*1B\r\n");
currentStep++;
currentDelay = 250;
return false;
} else if (currentStep == 1) {
// enable RMC
_serial_gps->write("$CFGMSG,0,4,1,1*1F\r\n");
currentDelay = 250;
}
}
didSerialInit = true;
}
+2
View File
@@ -128,6 +128,8 @@ class GPS : private concurrency::OSThread
// Let the GPS hardware save power between updates
void down();
bool initFinished() const { return GPSInitFinished; };
private:
GPS() : concurrency::OSThread("GPS") {}
+1 -1
View File
@@ -891,7 +891,7 @@ int32_t Screen::runOnce()
// Show boot screen for first logo_timeout seconds, then switch to normal operation.
// serialSinceMsec adjusts for additional serial wait time during nRF52 bootup
static bool showingBootScreen = true;
if (showingBootScreen && (millis() > (logo_timeout + serialSinceMsec))) {
if (showingBootScreen && (!gps || gps->initFinished()) && (millis() > (logo_timeout + serialSinceMsec))) {
LOG_INFO("Done with boot screen");
stopBootScreen();
showingBootScreen = false;
+8 -12
View File
@@ -1458,8 +1458,7 @@ void TFTDisplay::sendCommand(uint8_t com)
digitalWrite(portduino_config.displayBacklight.pin, TFT_BACKLIGHT_ON);
#elif defined(HACKADAY_COMMUNICATOR)
tft->displayOn();
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096) && \
!defined(HELTEC_MESH_NODE_T1)
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096)
tft->wakeup();
tft->powerSaveOff();
#endif
@@ -1470,7 +1469,7 @@ void TFTDisplay::sendCommand(uint8_t com)
#ifdef UNPHONE
unphone.backlight(true); // using unPhone library
#endif
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) || defined(HELTEC_MESH_NODE_T1)
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096)
#elif !defined(M5STACK) && !defined(ST7789_CS) && \
!defined(HACKADAY_COMMUNICATOR) // T-Deck gets brightness set in Screen.cpp in the handleSetOn function
tft->setBrightness(172);
@@ -1486,8 +1485,7 @@ void TFTDisplay::sendCommand(uint8_t com)
digitalWrite(portduino_config.displayBacklight.pin, !TFT_BACKLIGHT_ON);
#elif defined(HACKADAY_COMMUNICATOR)
tft->displayOff();
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096) && \
!defined(HELTEC_MESH_NODE_T1)
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096)
tft->sleep();
tft->powerSaveOn();
#endif
@@ -1498,7 +1496,7 @@ void TFTDisplay::sendCommand(uint8_t com)
#ifdef UNPHONE
unphone.backlight(false); // using unPhone library
#endif
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) || defined(HELTEC_MESH_NODE_T1)
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096)
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR)
tft->setBrightness(0);
#endif
@@ -1513,7 +1511,7 @@ void TFTDisplay::sendCommand(uint8_t com)
void TFTDisplay::setDisplayBrightness(uint8_t _brightness)
{
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) || defined(HELTEC_MESH_NODE_T1)
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096)
// todo
#elif !defined(HACKADAY_COMMUNICATOR)
tft->setBrightness(_brightness);
@@ -1533,8 +1531,7 @@ bool TFTDisplay::hasTouch(void)
{
#ifdef RAK14014
return true;
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096) && \
!defined(HELTEC_MESH_NODE_T1)
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096)
return tft->touch() != nullptr;
#else
return false;
@@ -1553,8 +1550,7 @@ bool TFTDisplay::getTouch(int16_t *x, int16_t *y)
} else {
return false;
}
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096) && \
!defined(HELTEC_MESH_NODE_T1)
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096)
return tft->getTouch(x, y);
#else
return false;
@@ -1571,7 +1567,7 @@ bool TFTDisplay::connect()
{
concurrency::LockGuard g(spiLock);
LOG_INFO("Do TFT init");
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) || defined(HELTEC_MESH_NODE_T1)
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096)
tft = new TFT_eSPI;
#elif defined(HACKADAY_COMMUNICATOR)
bus = new Arduino_ESP32SPI(TFT_DC, TFT_CS, 38 /* SCK */, 21 /* MOSI */, GFX_NOT_DEFINED /* MISO */, HSPI /* spi_num */);
-8
View File
@@ -206,9 +206,6 @@ 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},
};
@@ -1336,11 +1333,6 @@ void menuHandler::positionBaseMenu()
if (accelerometerThread) {
accelerometerThread->calibrate(30);
}
#endif
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_MAGNETOMETER
if (magnetometerThread) {
magnetometerThread->calibrate(30);
}
#endif
break;
case PositionAction::GPSSmartPosition:
+18
View File
@@ -1505,6 +1505,24 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED
display->drawString(x + getStringCenteredX(title) + 1, y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - 5, title);
}
display->setFont(FONT_SMALL);
if (bootString != nullptr) {
static uint8_t bootStringStep = 0;
char stringCharacter = '.';
uint32_t stringWidth = getStringCenteredX(bootString);
if (bootStringStep == 0) {
stringCharacter = '.';
bootStringStep++;
} else if (bootStringStep == 1) {
stringCharacter = 'o';
bootStringStep++;
} else if (bootStringStep == 2) {
stringCharacter = 'O';
bootStringStep = 0;
}
char tmpBootString[40];
snprintf(tmpBootString, sizeof(tmpBootString), "%s %c", bootString, stringCharacter);
display->drawString(x + stringWidth, y + SCREEN_HEIGHT - 2 * FONT_HEIGHT_MEDIUM, tmpBootString);
}
// Draw region in upper left
if (upperMsg) {
display->drawString(x + 5, y + 5, upperMsg);
+33 -1
View File
@@ -6,12 +6,15 @@
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 (928×508) is copied into the centre of the physical
The InkHUD safe-area buffer (944×523) 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
@@ -209,6 +212,35 @@ 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
+9 -9
View File
@@ -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 = 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)
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)
*/
@@ -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 = 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 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 uint8_t H_OFFSET_BYTES = 2; // visual TOP : 16px physical left margin
// visual BOTTOM: 96016928=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 uint8_t H_OFFSET_BYTES = 1; // visual TOP : 8px physical left margin
// visual BOTTOM: 9608944=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 UpdateTypes supported = static_cast<UpdateTypes>(FULL | FAST);
@@ -66,9 +66,6 @@ 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,
@@ -87,8 +84,6 @@ 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,18 +784,6 @@ 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);
@@ -846,22 +834,6 @@ 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;
@@ -1497,9 +1469,6 @@ 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;
+4 -24
View File
@@ -52,7 +52,8 @@
#include "mesh/http/WebServer.h"
#endif
#if !MESHTASTIC_EXCLUDE_BLUETOOTH
BluetoothApi *bluetoothApi = nullptr;
#include "nimble/NimbleBluetooth.h"
NimbleBluetooth *nimbleBluetooth = nullptr;
#endif
#endif
@@ -91,6 +92,8 @@ NRF54L15Bluetooth *nrf54l15Bluetooth = nullptr;
#include <string>
#endif
const char *bootString =
nullptr; // Pointer to a string that will be drawn on the boot screen, if set (used for GPS probing status)
#ifdef ARCH_ESP32
#ifdef DEBUG_PARTITION_TABLE
#include "esp_partition.h"
@@ -135,10 +138,6 @@ void printPartitionTable()
#include "motion/AccelerometerThread.h"
AccelerometerThread *accelerometerThread = nullptr;
#endif
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_MAGNETOMETER
#include "motion/MagnetometerThread.h"
MagnetometerThread *magnetometerThread = nullptr;
#endif
#ifdef HAS_I2S
#include "AudioThread.h"
@@ -209,8 +208,6 @@ bool osk_found = false;
ScanI2C::DeviceAddress rtc_found = ScanI2C::ADDRESS_NONE;
// The I2C address of the Accelerometer (if found)
ScanI2C::DeviceAddress accelerometer_found = ScanI2C::ADDRESS_NONE;
// The I2C address of the Magnetometer (if found)
ScanI2C::DeviceAddress magnetometer_found = ScanI2C::ADDRESS_NONE;
// The I2C address of the RGB LED (if found)
ScanI2C::FoundDevice rgb_found = ScanI2C::FoundDevice(ScanI2C::DeviceType::NONE, ScanI2C::ADDRESS_NONE);
/// The I2C address of our Air Quality Indicator (if found)
@@ -436,11 +433,6 @@ void setup()
digitalWrite(VEXT_ENABLE, VEXT_ON_VALUE); // turn on the display power
#endif
#if defined(PIN_SENSOR_EN)
pinMode(PIN_SENSOR_EN, OUTPUT);
digitalWrite(PIN_SENSOR_EN, PIN_SENSOR_EN_ACTIVE); // turn on sensor power
#endif
#if defined(BIAS_T_ENABLE)
pinMode(BIAS_T_ENABLE, OUTPUT);
digitalWrite(BIAS_T_ENABLE, BIAS_T_VALUE); // turn on 5V for GPS Antenna
@@ -681,11 +673,6 @@ void setup()
accelerometer_found = acc_info.type != ScanI2C::DeviceType::NONE ? acc_info.address : accelerometer_found;
LOG_DEBUG("acc_info = %i", acc_info.type);
#endif
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_MAGNETOMETER
auto mag_info = i2cScanner->firstMagnetometer();
magnetometer_found = mag_info.type != ScanI2C::DeviceType::NONE ? mag_info.address : magnetometer_found;
LOG_DEBUG("mag_info = %i", mag_info.type);
#endif
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA260, meshtastic_TelemetrySensorType_INA260);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA226, meshtastic_TelemetrySensorType_INA226);
@@ -698,8 +685,6 @@ void setup()
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMI8658, meshtastic_TelemetrySensorType_QMI8658);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC5883L, meshtastic_TelemetrySensorType_QMC5883L);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::HMC5883L, meshtastic_TelemetrySensorType_QMC5883L);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MMC5983MA, meshtastic_TelemetrySensorType_MMC5983MA);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM42607P, meshtastic_TelemetrySensorType_ICM42607P);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MLX90614, meshtastic_TelemetrySensorType_MLX90614);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM20948, meshtastic_TelemetrySensorType_ICM20948);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX30102, meshtastic_TelemetrySensorType_MAX30102);
@@ -783,11 +768,6 @@ void setup()
accelerometerThread = new AccelerometerThread(acc_info.type);
}
#endif
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_MAGNETOMETER
if (mag_info.type != ScanI2C::DeviceType::NONE) {
magnetometerThread = new MagnetometerThread(mag_info.type);
}
#endif
#if defined(HAS_NEOPIXEL) || defined(UNPHONE) || defined(RGBLED_RED)
ambientLightingThread = new AmbientLightingThread(ScanI2C::DeviceType::NONE);
+4 -7
View File
@@ -12,8 +12,8 @@
#include <SPI.h>
#include <map>
#if defined(ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2)
#include "BluetoothCommon.h"
extern BluetoothApi *bluetoothApi;
#include "nimble/NimbleBluetooth.h"
extern NimbleBluetooth *nimbleBluetooth;
#endif
#ifdef ARCH_NRF52
#include "NRF52Bluetooth.h"
@@ -39,7 +39,6 @@ extern bool kb_found;
extern bool osk_found;
extern ScanI2C::DeviceAddress rtc_found;
extern ScanI2C::DeviceAddress accelerometer_found;
extern ScanI2C::DeviceAddress magnetometer_found;
extern ScanI2C::FoundDevice rgb_found;
extern ScanI2C::DeviceAddress aqi_found;
@@ -74,10 +73,6 @@ extern graphics::Screen *screen;
#include "motion/AccelerometerThread.h"
extern AccelerometerThread *accelerometerThread;
#endif
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_MAGNETOMETER
#include "motion/MagnetometerThread.h"
extern MagnetometerThread *magnetometerThread;
#endif
extern bool isVibrating;
@@ -110,3 +105,5 @@ void scannerToSensorsMap(const std::unique_ptr<ScanI2CTwoWire> &i2cScanner, Scan
// We default to 4MHz SPI, SPI mode 0
extern SPISettings spiSettings;
extern const char *bootString;
-6
View File
@@ -37,12 +37,6 @@ 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
+5 -36
View File
@@ -15,7 +15,7 @@ static const meshtastic_Config_LoRaConfig_ModemPreset MODEM_PRESET_END =
#define PRESET(name) meshtastic_Config_LoRaConfig_ModemPreset_##name
// Override slot magic numbers for RegionInfo.overrideSlot
// Override slot magic numbers for RegionProfile.overrideSlot
#define OVERRIDE_SLOT_DEFAULT_CHANNEL_HASH 0 // Use hash of primary channel name
#define OVERRIDE_SLOT_PRESET_HASH -1 // Use hash of preset name instead
// Positive values (1-32767) are explicit slot numbers
@@ -30,6 +30,8 @@ struct RegionProfile {
int8_t textThrottle; // throttle for text - future expansion
int8_t positionThrottle; // throttle for location data - future expansion
int8_t telemetryThrottle; // throttle for telemetry - future expansion
int16_t overrideSlot; // a per-region override slot for if we need to fix it in place
// Magic values: 0 = use channel name hash, -1 = use preset name hash, >0 = explicit slot
};
/**
@@ -44,7 +46,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_20KHZ;
// extern const RegionProfile PROFILE_HAM;
// Map from old region names to new region enums
struct RegionInfo {
@@ -57,9 +59,7 @@ struct RegionInfo {
bool wideLora;
const RegionProfile *profile;
meshtastic_Config_LoRaConfig_ModemPreset defaultPreset;
int16_t overrideSlot; // per-region override slot for frequency selection
// Magic values: 0 = use channel name hash, -1 = use preset name hash, >0 = explicit slot
const char *name; // EU433 etc
const char *name; // EU433 etc
// Preset accessors (delegate through profile)
meshtastic_Config_LoRaConfig_ModemPreset getDefaultPreset() const { return defaultPreset; }
@@ -77,7 +77,6 @@ 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;
@@ -124,18 +123,8 @@ 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)
@@ -151,18 +140,8 @@ 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)
@@ -240,16 +219,6 @@ 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;
-11
View File
@@ -25,9 +25,6 @@
#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>
@@ -2541,14 +2538,6 @@ 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) {
+52 -95
View File
@@ -49,23 +49,18 @@ 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};
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};
// presets, spacing, padding, audio, licensed, text throttle, position throttle, telemetry throttle, override slot
const RegionProfile PROFILE_STD = {PRESETS_STD, 0, 0, true, false, 0, 1, 1, 0};
const RegionProfile PROFILE_EU868 = {PRESETS_EU_868, 0, 0, false, false, 0, 1, 1, 0};
const RegionProfile PROFILE_UNDEF = {PRESETS_UNDEF, 0, 0, true, false, 0, 1, 1, 0};
const RegionProfile PROFILE_LITE = {PRESETS_LITE, 0.4, 0.0375f, false, false, 0, 10, 10, 0};
const RegionProfile PROFILE_NARROW = {PRESETS_NARROW, 0, 0.0104f, true, false, 0, 1, 1, 1};
#define RDEF(name, freq_start, freq_end, duty_cycle, power_limit, frequency_switching, wide_lora, profile_ptr, default_preset, \
override_slot) \
#define RDEF(name, freq_start, freq_end, duty_cycle, power_limit, frequency_switching, wide_lora, profile_ptr, default_preset) \
{ \
meshtastic_Config_LoRaConfig_RegionCode_##name, freq_start, freq_end, duty_cycle, power_limit, frequency_switching, \
wide_lora, &profile_ptr, default_preset, override_slot, #name \
wide_lora, &profile_ptr, default_preset, #name \
}
const RegionInfo regions[] = {
@@ -73,7 +68,7 @@ const RegionInfo regions[] = {
https://link.springer.com/content/pdf/bbm%3A978-1-4842-4357-2%2F1.pdf
https://www.thethingsnetwork.org/docs/lorawan/regional-parameters/
*/
RDEF(US, 902.0f, 928.0f, 100, 30, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
RDEF(US, 902.0f, 928.0f, 100, 30, false, false, PROFILE_STD, PRESET(LONG_FAST)),
/*
EN300220 ETSI V3.2.1 [Table B.1, Item H, p. 21]
@@ -81,7 +76,7 @@ const RegionInfo regions[] = {
https://www.etsi.org/deliver/etsi_en/300200_300299/30022002/03.02.01_60/en_30022002v030201p.pdf
FIXME: https://github.com/meshtastic/firmware/issues/3371
*/
RDEF(EU_433, 433.0f, 434.0f, 10, 10, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
RDEF(EU_433, 433.0f, 434.0f, 10, 10, false, false, PROFILE_STD, PRESET(LONG_FAST)),
/*
https://www.thethingsnetwork.org/docs/lorawan/duty-cycle/
https://www.thethingsnetwork.org/docs/lorawan/regional-parameters/
@@ -96,33 +91,33 @@ const RegionInfo regions[] = {
AFA) to avoid a duty cycle. (Please refer to line P page 22 of this document.)
https://www.etsi.org/deliver/etsi_en/300200_300299/30022002/03.01.01_60/en_30022002v030101p.pdf
*/
RDEF(EU_868, 869.4f, 869.65f, 10, 27, false, false, PROFILE_EU868, PRESET(LONG_FAST), 0),
RDEF(EU_868, 869.4f, 869.65f, 10, 27, false, false, PROFILE_EU868, PRESET(LONG_FAST)),
/*
https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf
*/
RDEF(CN, 470.0f, 510.0f, 100, 19, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
RDEF(CN, 470.0f, 510.0f, 100, 19, false, false, PROFILE_STD, PRESET(LONG_FAST)),
/*
https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf
https://www.arib.or.jp/english/html/overview/doc/5-STD-T108v1_5-E1.pdf
https://qiita.com/ammo0613/items/d952154f1195b64dc29f
*/
RDEF(JP, 920.5f, 923.5f, 100, 13, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
RDEF(JP, 920.5f, 923.5f, 100, 13, false, false, PROFILE_STD, PRESET(LONG_FAST)),
/*
https://www.iot.org.au/wp/wp-content/uploads/2016/12/IoTSpectrumFactSheet.pdf
https://iotalliance.org.nz/wp-content/uploads/sites/4/2019/05/IoT-Spectrum-in-NZ-Briefing-Paper.pdf
Also used in Brazil.
*/
RDEF(ANZ, 915.0f, 928.0f, 100, 30, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
RDEF(ANZ, 915.0f, 928.0f, 100, 30, false, false, PROFILE_STD, PRESET(LONG_FAST)),
/*
433.05 - 434.79 MHz, 25mW EIRP max, No duty cycle restrictions
AU Low Interference Potential https://www.acma.gov.au/licences/low-interference-potential-devices-lipd-class-licence
NZ General User Radio Licence for Short Range Devices https://gazette.govt.nz/notice/id/2022-go3100
*/
RDEF(ANZ_433, 433.05f, 434.79f, 100, 14, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
RDEF(ANZ_433, 433.05f, 434.79f, 100, 14, false, false, PROFILE_STD, PRESET(LONG_FAST)),
/*
https://digital.gov.ru/uploaded/files/prilozhenie-12-k-reshenyu-gkrch-18-46-03-1.pdf
@@ -130,13 +125,13 @@ const RegionInfo regions[] = {
Note:
- We do LBT, so 100% is allowed.
*/
RDEF(RU, 868.7f, 869.2f, 100, 20, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
RDEF(RU, 868.7f, 869.2f, 100, 20, false, false, PROFILE_STD, PRESET(LONG_FAST)),
/*
https://www.law.go.kr/LSW/admRulLsInfoP.do?admRulId=53943&efYd=0
https://resources.lora-alliance.org/technical-specifications/rp002-1-0-4-regional-parameters
*/
RDEF(KR, 920.0f, 923.0f, 100, 23, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
RDEF(KR, 920.0f, 923.0f, 100, 23, false, false, PROFILE_STD, PRESET(LONG_FAST)),
/*
Taiwan, 920-925Mhz, limited to 0.5W indoor or coastal, 1.0W outdoor.
@@ -144,40 +139,40 @@ const RegionInfo regions[] = {
https://www.ncc.gov.tw/english/files/23070/102_5190_230703_1_doc_C.PDF
https://gazette.nat.gov.tw/egFront/e_detail.do?metaid=147283
*/
RDEF(TW, 920.0f, 925.0f, 100, 27, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
RDEF(TW, 920.0f, 925.0f, 100, 27, false, false, PROFILE_STD, PRESET(LONG_FAST)),
/*
https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf
*/
RDEF(IN, 865.0f, 867.0f, 100, 30, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
RDEF(IN, 865.0f, 867.0f, 100, 30, false, false, PROFILE_STD, PRESET(LONG_FAST)),
/*
https://rrf.rsm.govt.nz/smart-web/smart/page/-smart/domain/licence/LicenceSummary.wdk?id=219752
https://iotalliance.org.nz/wp-content/uploads/sites/4/2019/05/IoT-Spectrum-in-NZ-Briefing-Paper.pdf
*/
RDEF(NZ_865, 864.0f, 868.0f, 100, 36, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
RDEF(NZ_865, 864.0f, 868.0f, 100, 36, false, false, PROFILE_STD, PRESET(LONG_FAST)),
/*
https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf
https://standard.nbtc.go.th/getattachment/Standards/%E0%B8%A1%E0%B8%B2%E0%B8%95%E0%B8%A3%E0%B8%90%E0%B8%B2%E0%B8%99%E0%B8%97%E0%B8%B2%E0%B8%87%E0%B9%80%E0%B8%97%E0%B8%84%E0%B8%99%E0%B8%B4%E0%B8%84%E0%B8%82%E0%B8%AD%E0%B8%87%E0%B9%80%E0%B8%84%E0%B8%A3%E0%B8%B7%E0%B9%88%E0%B8%AD%E0%B8%87%E0%B9%82%E0%B8%97%E0%B8%A3%E0%B8%84%E0%B8%A1%E0%B8%99%E0%B8%B2%E0%B8%84%E0%B8%A1/1033-2565.pdf.aspx?lang=th-TH
Thailand 920925 MHz set max TX power to 27 dBm and enforce 10% duty cycle, aligned with NBTC regulations.
*/
RDEF(TH, 920.0f, 925.0f, 10, 27, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
RDEF(TH, 920.0f, 925.0f, 10, 27, false, false, PROFILE_STD, PRESET(LONG_FAST)),
/*
433,05-434,7 Mhz 10 mW
868,0-868,6 Mhz 25 mW
https://nkrzi.gov.ua/images/upload/256/5810/PDF_UUZ_19_01_2016.pdf
*/
RDEF(UA_433, 433.0f, 434.7f, 10, 10, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
RDEF(UA_868, 868.0f, 868.6f, 1, 14, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
RDEF(UA_433, 433.0f, 434.7f, 10, 10, false, false, PROFILE_STD, PRESET(LONG_FAST)),
RDEF(UA_868, 868.0f, 868.6f, 1, 14, false, false, PROFILE_STD, PRESET(LONG_FAST)),
/*
Malaysia
433 - 435 MHz at 100mW, no restrictions.
https://www.mcmc.gov.my/skmmgovmy/media/General/pdf/Short-Range-Devices-Specification.pdf
*/
RDEF(MY_433, 433.0f, 435.0f, 100, 20, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
RDEF(MY_433, 433.0f, 435.0f, 100, 20, false, false, PROFILE_STD, PRESET(LONG_FAST)),
/*
Malaysia
@@ -186,14 +181,14 @@ const RegionInfo regions[] = {
Frequency hopping is used for 919 - 923 MHz.
https://www.mcmc.gov.my/skmmgovmy/media/General/pdf/Short-Range-Devices-Specification.pdf
*/
RDEF(MY_919, 919.0f, 924.0f, 100, 27, true, false, PROFILE_STD, PRESET(LONG_FAST), 0),
RDEF(MY_919, 919.0f, 924.0f, 100, 27, true, false, PROFILE_STD, PRESET(LONG_FAST)),
/*
Singapore
SG_923 Band 30d: 917 - 925 MHz at 100mW, no restrictions.
https://www.imda.gov.sg/-/media/imda/files/regulation-licensing-and-consultations/ict-standards/telecommunication-standards/radio-comms/imdatssrd.pdf
*/
RDEF(SG_923, 917.0f, 925.0f, 100, 20, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
RDEF(SG_923, 917.0f, 925.0f, 100, 20, false, false, PROFILE_STD, PRESET(LONG_FAST)),
/*
Philippines
@@ -203,9 +198,9 @@ const RegionInfo regions[] = {
https://github.com/meshtastic/firmware/issues/4948#issuecomment-2394926135
*/
RDEF(PH_433, 433.0f, 434.7f, 100, 10, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
RDEF(PH_868, 868.0f, 869.4f, 100, 14, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
RDEF(PH_915, 915.0f, 918.0f, 100, 24, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
RDEF(PH_433, 433.0f, 434.7f, 100, 10, false, false, PROFILE_STD, PRESET(LONG_FAST)),
RDEF(PH_868, 868.0f, 869.4f, 100, 14, false, false, PROFILE_STD, PRESET(LONG_FAST)),
RDEF(PH_915, 915.0f, 918.0f, 100, 24, false, false, PROFILE_STD, PRESET(LONG_FAST)),
/*
Kazakhstan
@@ -213,75 +208,46 @@ const RegionInfo regions[] = {
863 - 868 MHz <25 mW EIRP, 500kHz channels allowed, must not be used at airfields
https://github.com/meshtastic/firmware/issues/7204
*/
RDEF(KZ_433, 433.075f, 434.775f, 100, 10, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
RDEF(KZ_863, 863.0f, 868.0f, 100, 30, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
RDEF(KZ_433, 433.075f, 434.775f, 100, 10, false, false, PROFILE_STD, PRESET(LONG_FAST)),
RDEF(KZ_863, 863.0f, 868.0f, 100, 30, false, false, PROFILE_STD, PRESET(LONG_FAST)),
/*
Nepal
865MHz to 868MHz frequency band for IoT (Internet of Things), M2M (Machine-to-Machine), and smart metering use,
specifically in non-cellular mode. https://www.nta.gov.np/uploads/contents/Radio-Frequency-Policy-2080-English.pdf
*/
RDEF(NP_865, 865.0f, 868.0f, 100, 30, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
RDEF(NP_865, 865.0f, 868.0f, 100, 30, false, false, PROFILE_STD, PRESET(LONG_FAST)),
/*
Brazil
902 - 907.5 MHz , 1W power limit, no duty cycle restrictions
https://github.com/meshtastic/firmware/issues/3741
*/
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),
RDEF(BR_902, 902.0f, 907.5f, 100, 30, false, false, PROFILE_STD, PRESET(LONG_FAST)),
/*
2.4 GHZ WLAN Band equivalent. Only for SX128x chips.
*/
RDEF(LORA_24, 2400.0f, 2483.5f, 100, 10, false, true, PROFILE_STD, PRESET(LONG_FAST), 0),
RDEF(LORA_24, 2400.0f, 2483.5f, 100, 10, false, true, PROFILE_STD, PRESET(LONG_FAST)),
/*
EU 866MHz band (Band no. 46b of 2006/771/EC and subsequent amendments) for Non-specific short-range devices (SRD)
Gives 4 channels at 865.7/866.3/866.9/867.5 MHz, 400 kHz gap plus 37.5 kHz padding between channels, 27 dBm,
duty cycle 2.5% (mobile) or 10% (fixed) https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:02006D0771(01)-20250123
*/
RDEF(EU_866, 865.6f, 867.6f, 2.5, 27, false, false, PROFILE_LITE, PRESET(LITE_FAST), 0),
RDEF(EU_866, 865.6f, 867.6f, 2.5, 27, false, false, PROFILE_LITE, PRESET(LITE_FAST)),
/*
EU 868MHz band: 3 channels at 869.410/869.4625/869.577 MHz
Channel centres at 869.442/869.525/869.608 MHz,
10.4 kHz padding on channels, 27 dBm, duty cycle 10%
*/
RDEF(EU_N_868, 869.4f, 869.65f, 10, 27, false, false, PROFILE_NARROW, PRESET(NARROW_SLOW), 1),
RDEF(EU_N_868, 869.4f, 869.65f, 10, 27, false, false, PROFILE_NARROW, PRESET(NARROW_SLOW)),
/*
This needs to be last. Same as US.
*/
RDEF(UNSET, 902.0f, 928.0f, 100, 30, false, false, PROFILE_UNDEF, PRESET(LONG_FAST), 0),
RDEF(UNSET, 902.0f, 928.0f, 100, 30, false, false, PROFILE_UNDEF, PRESET(LONG_FAST)),
};
@@ -867,16 +833,6 @@ 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];
@@ -976,11 +932,11 @@ bool RadioInterface::checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraCo
// overrideSlot: 0 = channel hash, -1 = preset hash, >0 = explicit slot
uses_default_frequency_slot =
(loraConfig.channel_num == 0) || // user choice unset, no frequency override, so use default
(newRegion->overrideSlot > 0 &&
loraConfig.channel_num == newRegion->overrideSlot) || // user setting matches explicit override slot
((newRegion->overrideSlot == OVERRIDE_SLOT_DEFAULT_CHANNEL_HASH) &&
(newRegion->profile->overrideSlot > 0 &&
loraConfig.channel_num == newRegion->profile->overrideSlot) || // user setting matches explicit override slot
((newRegion->profile->overrideSlot == OVERRIDE_SLOT_DEFAULT_CHANNEL_HASH) &&
((uint32_t)(loraConfig.channel_num - 1) == channelNameHashSlot)) || // user setting matches channel name hash
((newRegion->overrideSlot == OVERRIDE_SLOT_PRESET_HASH) &&
((newRegion->profile->overrideSlot == OVERRIDE_SLOT_PRESET_HASH) &&
((uint32_t)(loraConfig.channel_num - 1) == presetNameHashSlot)); // user setting matches preset name hash
// check if user setting different to preset name
@@ -996,11 +952,12 @@ bool RadioInterface::checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraCo
if (clamp) {
if (uses_custom_channel_name) { // clamp to channel name hash
loraConfig.channel_num =
channelNameHashSlot + 1; // channel_num is 1-based, but hash slot is 0-based, so add 1
} else if (newRegion->overrideSlot > 0) { // clamp to explicit override slot
loraConfig.channel_num = newRegion->overrideSlot; // use the explicit override slot defined for this region
channelNameHashSlot + 1; // channel_num is 1-based, but hash slot is 0-based, so add 1
} else if (newRegion->profile->overrideSlot > 0) { // clamp to explicit override slot
loraConfig.channel_num =
newRegion->profile->overrideSlot; // use the explicit override slot specified by the region profile
uses_default_frequency_slot = true;
} else if (newRegion->overrideSlot == OVERRIDE_SLOT_PRESET_HASH && loraConfig.use_preset) {
} else if (newRegion->profile->overrideSlot == OVERRIDE_SLOT_PRESET_HASH && loraConfig.use_preset) {
// clamp to preset name hash
loraConfig.channel_num = presetNameHashSlot + 1; // channel_num is 1-based, but hash slot is 0-based, so add 1
uses_default_frequency_slot = true;
@@ -1119,9 +1076,9 @@ void RadioInterface::applyModemConfig()
// NB: channel_num is also know as frequency slot but it's too late to fix now.
if (uses_default_frequency_slot) {
// Handle three override slot cases: explicit slot (>0), preset hash (-1), or channel hash (0)
if (newRegion->overrideSlot > 0) {
channel_num = newRegion->overrideSlot - 1; // explicit override slot (1-based to 0-based)
} else if (newRegion->overrideSlot == OVERRIDE_SLOT_PRESET_HASH) {
if (newRegion->profile->overrideSlot > 0) {
channel_num = newRegion->profile->overrideSlot - 1; // explicit override slot (1-based to 0-based)
} else if (newRegion->profile->overrideSlot == OVERRIDE_SLOT_PRESET_HASH) {
channel_num = presetNameHashSlot; // use preset name hash
} else {
channel_num = channelNameHashSlot; // use channel name hash (default case)
@@ -1154,9 +1111,9 @@ void RadioInterface::applyModemConfig()
LOG_INFO("newRegion->freqStart -> newRegion->freqEnd: %f -> %f (%f MHz)", newRegion->freqStart, newRegion->freqEnd,
newRegion->freqEnd - newRegion->freqStart);
LOG_INFO("numFreqSlots: %u x %.3fkHz", numFreqSlots, bw);
if (newRegion->overrideSlot > 0) {
LOG_INFO("Using region explicit override slot: %d", newRegion->overrideSlot);
} else if (newRegion->overrideSlot == OVERRIDE_SLOT_PRESET_HASH) {
if (newRegion->profile->overrideSlot > 0) {
LOG_INFO("Using region explicit override slot: %d", newRegion->profile->overrideSlot);
} else if (newRegion->profile->overrideSlot == OVERRIDE_SLOT_PRESET_HASH) {
LOG_INFO("Using region preset name hash for slot selection");
}
LOG_INFO("channel_num: %d", channel_num + 1);
+10 -29
View File
@@ -15,9 +15,6 @@
#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
@@ -25,6 +22,8 @@
#if ARCH_PORTDUINO
#include "Throttle.h"
#include "platform/portduino/PortduinoGlue.h"
#endif
#if ENABLE_JSON_LOGGING || ARCH_PORTDUINO
#include "serialization/MeshPacketSerializer.h"
#endif
@@ -358,30 +357,6 @@ 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;
@@ -580,7 +555,9 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
} */
printPacket("decoded message", p);
#if ARCH_PORTDUINO
#if ENABLE_JSON_LOGGING
LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str());
#elif 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 != "") {
@@ -875,7 +852,11 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
void Router::perhapsHandleReceived(meshtastic_MeshPacket *p)
{
#if ARCH_PORTDUINO
#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
// 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
+4 -28
View File
@@ -198,28 +198,6 @@ 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 */
@@ -543,7 +521,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, 0}
#define meshtastic_LockdownAuth_init_default {{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}
@@ -556,7 +534,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, 0}
#define meshtastic_LockdownAuth_init_zero {{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}
@@ -579,7 +557,6 @@ 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
@@ -777,8 +754,7 @@ 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, BOOL, disable, 6)
X(a, STATIC, SINGULAR, UINT32, max_session_seconds, 5)
#define meshtastic_LockdownAuth_CALLBACK NULL
#define meshtastic_LockdownAuth_DEFAULT NULL
@@ -893,7 +869,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 56
#define meshtastic_LockdownAuth_size 54
#define meshtastic_NodeRemoteHardwarePinsResponse_size 496
#define meshtastic_SCD30_config_size 27
#define meshtastic_SCD4X_config_size 29
+3 -17
View File
@@ -356,21 +356,7 @@ 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,
/* 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_NARROW_SLOW = 13
} meshtastic_Config_LoRaConfig_ModemPreset;
typedef enum _meshtastic_Config_LoRaConfig_FEM_LNA_Mode {
@@ -763,8 +749,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_TINY_SLOW
#define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_TINY_SLOW+1))
#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_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
+3 -10
View File
@@ -658,14 +658,7 @@ 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,
/* 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_UNLOCK_FAILED = 4
} meshtastic_LockdownStatus_State;
/* Struct definitions */
@@ -1540,8 +1533,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_DISABLED
#define _meshtastic_LockdownStatus_State_ARRAYSIZE ((meshtastic_LockdownStatus_State)(meshtastic_LockdownStatus_State_DISABLED+1))
#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_Position_location_source_ENUMTYPE meshtastic_Position_LocSource
#define meshtastic_Position_altitude_source_ENUMTYPE meshtastic_Position_AltSource
+176 -255
View File
@@ -11,12 +11,11 @@
#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"
@@ -260,95 +259,40 @@ void htmlDeleteDir(const char *dirname)
root.close();
}
// 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)
JSONArray htmlListDir(const char *dirname, uint8_t levels)
{
File root = FSCom.open(dirname, FILE_O_READ);
std::string out = "[";
bool first = true;
JSONArray fileList;
if (!root) {
out += "]";
return out;
return fileList;
}
if (!root.isDirectory()) {
out += "]";
return out;
return fileList;
}
// 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
element = htmlListDir(file.path(), levels - 1);
fileList.push_back(new JSONValue(htmlListDir(file.path(), levels - 1)));
#else
element = htmlListDir(file.name(), levels - 1);
fileList.push_back(new JSONValue(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);
@@ -356,30 +300,15 @@ std::string htmlListDir(const char *dirname, uint8_t levels)
String modifiedFile = String(file.name()).substring(1);
#endif
modifiedFile.remove((modifiedFile.length() - 3), 3);
element += ",";
element += jsonEscape("nameModified");
element += ":";
element += jsonEscape(modifiedFile.c_str());
thisFileMap["nameModified"] = new JSONValue(modifiedFile.c_str());
}
element += ",";
element += jsonEscape("size");
element += ":";
element += jsonNum((int)file.size());
element += "}";
haveElement = true;
}
if (haveElement) {
if (!first)
out += ",";
out += element;
first = false;
fileList.push_back(new JSONValue(thisFileMap));
}
file.close();
file = root.openNextFile();
}
root.close();
out += "]";
return out;
return fileList;
}
void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res)
@@ -389,25 +318,28 @@ void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res)
res->setHeader("Access-Control-Allow-Methods", "GET");
concurrency::LockGuard g(spiLock);
std::string fileList = htmlListDir("/static", 10);
auto fileList = htmlListDir("/static", 10);
uint64_t total = FSCom.totalBytes();
uint64_t used = FSCom.usedBytes();
// 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()));
// 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 jsonObjInner;
jsonObjInner["files"] = new JSONValue(fileList);
jsonObjInner["filesystem"] = new JSONValue(filesystemObj);
res->print(out.c_str());
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;
}
void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res)
@@ -422,13 +354,27 @@ void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res)
if (params->getQueryParameter("delete", paramValDelete)) {
std::string pathDelete = "/" + paramValDelete;
concurrency::LockGuard g(spiLock);
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;
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;
}
}
}
@@ -669,113 +615,95 @@ void handleReport(HTTPRequest *req, HTTPResponse *res)
res->println("<pre>");
}
auto arrayFromLog = [](const uint32_t *logArray, int count) -> std::string {
std::string s = "[";
// Helper lambda to create JSON array and clean up memory properly
auto createJSONArrayFromLog = [](const uint32_t *logArray, int count) -> JSONValue * {
JSONArray tempArray;
for (int i = 0; i < count; i++) {
if (i)
s += ",";
s += jsonNum((int)logArray[i]);
tempArray.push_back(new JSONValue((int)logArray[i]));
}
s += "]";
return s;
JSONValue *result = new JSONValue(tempArray);
// Note: Don't delete tempArray elements here - JSONValue now owns them
return result;
};
// data->airtime->tx_log
uint32_t *logArray;
logArray = airTime->airtimeReport(TX_LOG);
std::string txLog = arrayFromLog(logArray, airTime->getPeriodsToLog());
logArray = airTime->airtimeReport(RX_LOG);
std::string rxLog = arrayFromLog(logArray, airTime->getPeriodsToLog());
logArray = airTime->airtimeReport(RX_ALL_LOG);
std::string rxAllLog = arrayFromLog(logArray, airTime->getPeriodsToLog());
JSONValue *txLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
// data->airtime->rx_log
logArray = airTime->airtimeReport(RX_LOG);
JSONValue *rxLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
// data->airtime->rx_all_log
logArray = airTime->airtimeReport(RX_ALL_LOG);
JSONValue *rxAllLogJsonValue = createJSONArrayFromLog(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();
uint64_t fsTotal = FSCom.totalBytes();
uint64_t fsUsed = FSCom.usedBytes();
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()));
spiLock->unlock();
// 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->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()));
// 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->device
JSONObject jsonObjDevice;
jsonObjDevice["reboot_counter"] = new JSONValue((int)myNodeInfo.reboot_count);
// device
out += ",\"device\":{\"reboot_counter\":";
out += jsonNum((int)myNodeInfo.reboot_count);
out += "}";
// data->radio
JSONObject jsonObjRadio;
jsonObjRadio["frequency"] = new JSONValue(RadioLibInterface::instance->getFreq());
jsonObjRadio["lora_channel"] = new JSONValue((int)RadioLibInterface::instance->getChannelNum() + 1);
// 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 += "}";
// 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);
// 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());
// 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;
}
void handleNodes(HTTPRequest *req, HTTPResponse *res)
@@ -796,66 +724,58 @@ void handleNodes(HTTPRequest *req, HTTPResponse *res)
res->println("<pre>");
}
std::string out;
out.reserve(2048);
out += "{\"data\":{\"nodes\":[";
JSONArray nodesArray;
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);
std::string position;
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();
if (nodeDB->hasValidPosition(tempNodeInfo)) {
meshtastic_PositionLite posLite;
if (nodeDB->copyNodePosition(tempNodeInfo->num, posLite)) {
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";
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);
}
} else {
position = "null";
}
if (!firstNode)
out += ",";
firstNode = false;
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);
// 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 += "}";
nodesArray.push_back(new JSONValue(node));
}
tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
}
out += "]},\"status\":\"ok\"}";
res->print(out.c_str());
// 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;
}
/*
@@ -977,28 +897,20 @@ void handleScanNetworks(HTTPRequest *req, HTTPResponse *res)
int n = WiFi.scanNetworks();
std::string out = "{\"data\":[";
bool firstNet = true;
// build list of network objects
JSONArray networkObjs;
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) {
if (!firstNet)
out += ",";
firstNet = false;
out += "{\"rssi\":";
out += jsonNum((int)WiFi.RSSI(i));
out += ",\"ssid\":";
out += jsonEscape(ssidArray);
out += "}";
JSONObject thisNetwork;
thisNetwork["ssid"] = new JSONValue(ssidArray);
thisNetwork["rssi"] = new JSONValue(int(WiFi.RSSI(i)));
networkObjs.push_back(new JSONValue(thisNetwork));
}
// Yield some cpu cycles to IP stack.
// This is important in case the list is large and it takes us time to return
@@ -1006,7 +918,16 @@ void handleScanNetworks(HTTPRequest *req, HTTPResponse *res)
yield();
}
}
out += "],\"status\":\"ok\"}";
res->print(out.c_str());
// 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;
}
#endif
-9
View File
@@ -109,15 +109,6 @@ 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
-6
View File
@@ -66,10 +66,4 @@ 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__)
+5 -5
View File
@@ -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 && bluetoothApi) {
conn.bluetooth.is_connected = bluetoothApi->isConnected();
conn.bluetooth.rssi = bluetoothApi->getRssi();
if (config.bluetooth.enabled && nimbleBluetooth) {
conn.bluetooth.is_connected = nimbleBluetooth->isConnected();
conn.bluetooth.rssi = nimbleBluetooth->getRssi();
}
#elif defined(ARCH_NRF52)
if (config.bluetooth.enabled && nrf52Bluetooth) {
@@ -1605,8 +1605,8 @@ void disableBluetooth()
{
#if HAS_BLUETOOTH
#ifdef ARCH_ESP32
if (bluetoothApi)
bluetoothApi->deinit();
if (nimbleBluetooth)
nimbleBluetooth->deinit();
#elif defined(ARCH_NRF52)
if (nrf52Bluetooth)
nrf52Bluetooth->shutdown();
-493
View File
@@ -1,493 +0,0 @@
#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
-351
View File
@@ -1,351 +0,0 @@
#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 (07)
* - 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 (513 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 (07)
// 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
-7
View File
@@ -41,9 +41,6 @@
#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"
@@ -134,10 +131,6 @@ void setupModules()
}
#endif
#if HAS_VARIABLE_HOPS
hopScalingModule = new HopScalingModule();
#endif
#if !MESHTASTIC_EXCLUDE_ADMIN
adminModule = new AdminModule();
#endif
+2 -3
View File
@@ -332,6 +332,8 @@ meshtastic_MeshPacket *PositionModule::allocAtakPli()
takPacket.geo_src = meshtastic_GeoPointSource_GeoPointSource_GPS;
takPacket.alt_src = meshtastic_GeoPointSource_GeoPointSource_GPS;
}
takPacket.which_payload_variant = meshtastic_TAKPacketV2_pli_tag;
takPacket.payload_variant.pli = true;
// Callsign - stored as plain string (no compression, apps handle that)
strncpy(takPacket.callsign, owner.long_name, sizeof(takPacket.callsign) - 1);
@@ -339,9 +341,6 @@ meshtastic_MeshPacket *PositionModule::allocAtakPli()
strncpy(takPacket.device_callsign, owner.long_name, sizeof(takPacket.device_callsign) - 1);
takPacket.device_callsign[sizeof(takPacket.device_callsign) - 1] = '\0';
// CoT uid — ATAK drops PLI entities with empty uid; derive stable "!<nodenum>" id.
snprintf(takPacket.uid, sizeof(takPacket.uid), "!%08x", nodeDB->getNodeNum());
// Encode TAKPacketV2 protobuf, leaving room for flags byte prefix
uint8_t protobuf_bytes[sizeof(mp->decoded.payload.bytes) - 1];
size_t proto_size = pb_encode_to_bytes(protobuf_bytes, sizeof(protobuf_bytes), &meshtastic_TAKPacketV2_msg, &takPacket);
+1 -1
View File
@@ -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);
#ifndef PIO_UNIT_TESTING
#if !IS_RUNNING_TESTS
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
cn->level = meshtastic_LogRecord_Level_ERROR;
cn->time = getValidTime(RTCQualityFromNet);
-4
View File
@@ -15,7 +15,6 @@
#endif
#include "BMM150Sensor.h"
#include "BMX160Sensor.h"
#include "ICM42607PSensor.h"
#include "ICM20948Sensor.h"
#include "LIS3DHSensor.h"
#include "LSM6DS3Sensor.h"
@@ -112,9 +111,6 @@ class AccelerometerThread : public concurrency::OSThread
case ScanI2C::DeviceType::ICM20948:
sensor = new ICM20948Sensor(device);
break;
case ScanI2C::DeviceType::ICM42607P:
sensor = new ICM42607PSensor(device);
break;
case ScanI2C::DeviceType::BMM150:
sensor = new BMM150Sensor(device);
break;
-98
View File
@@ -1,98 +0,0 @@
#include "ICM42607PSensor.h"
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include(<ICM42670P.h>)
#include "detect/ScanI2CTwoWire.h"
#include <ICM42670P.h>
static constexpr uint16_t ICM42607P_ACCEL_ODR_HZ = 50;
static constexpr uint16_t ICM42607P_ACCEL_FSR_G = 2;
static constexpr float ICM42607P_COUNTS_PER_G = 32768.0f / ICM42607P_ACCEL_FSR_G;
#ifdef ICM_42607P_INT_PIN
volatile static bool ICM42607P_IRQ = false;
void ICM42607PSetInterrupt()
{
ICM42607P_IRQ = true;
}
#endif
ICM42607PSensor::ICM42607PSensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice)
{
wire = ScanI2CTwoWire::fetchI2CBus(foundDevice.address);
}
ICM42607PSensor::~ICM42607PSensor() = default;
bool ICM42607PSensor::init()
{
bool addressLsb = deviceAddress() == ICM42607P_ADDR_ALT;
LOG_DEBUG("ICM-42607-P begin on addr 0x%02X (port=%d)", deviceAddress(), devicePort());
sensor.reset();
auto newSensor = std::make_unique<ICM42670>(*wire, addressLsb);
int status = newSensor->begin();
// ICM42670P library returns -3 for ICM42607P because WHO_AM_I differs; the register map is compatible.
if (status != 0 && status != -3) {
LOG_DEBUG("ICM-42607-P init error %d", status);
return false;
}
status = newSensor->startAccel(ICM42607P_ACCEL_ODR_HZ, ICM42607P_ACCEL_FSR_G);
if (status != 0) {
LOG_DEBUG("ICM-42607-P accel start error %d", status);
return false;
}
#ifdef ICM_42607P_INT_PIN
ICM42607P_IRQ = false;
status = newSensor->startWakeOnMotion(ICM_42607P_INT_PIN, ICM42607PSetInterrupt);
if (status != 0) {
LOG_DEBUG("ICM-42607-P wake-on-motion start error %d", status);
return false;
}
LOG_DEBUG("ICM-42607-P wake-on-motion interrupt ok pin=%d", ICM_42607P_INT_PIN);
#endif
sensor = std::move(newSensor);
LOG_DEBUG("ICM-42607-P init ok");
return true;
}
int32_t ICM42607PSensor::runOnce()
{
#ifdef ICM_42607P_INT_PIN
if (ICM42607P_IRQ) {
ICM42607P_IRQ = false;
LOG_DEBUG("ICM-42607-P motion interrupt");
wakeScreen();
}
return MOTION_SENSOR_CHECK_INTERVAL_MS;
#else
int16_t x = 0;
int16_t y = 0;
int16_t z = 0;
inv_imu_sensor_event_t event = {};
if (sensor == nullptr || sensor->getDataFromRegisters(event) != 0) {
return MOTION_SENSOR_CHECK_INTERVAL_MS;
}
// getDataFromRegisters() fills accel[] but does not set sensor_mask in this library version.
if (event.accel[0] == 0 && event.accel[1] == 0 && event.accel[2] == 0) {
return MOTION_SENSOR_CHECK_INTERVAL_MS;
}
x = event.accel[0];
y = event.accel[1];
z = event.accel[2];
// LOG_DEBUG("ICM-42607-P accel read x=%.3fg y=%.3fg z=%.3fg", (float)x / ICM42607P_COUNTS_PER_G,
// (float)y / ICM42607P_COUNTS_PER_G, (float)z / ICM42607P_COUNTS_PER_G);
return MOTION_SENSOR_CHECK_INTERVAL_MS;
#endif
}
#endif
-28
View File
@@ -1,28 +0,0 @@
#pragma once
#ifndef _ICM42607P_SENSOR_H_
#define _ICM42607P_SENSOR_H_
#include "MotionSensor.h"
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include(<ICM42670P.h>)
#include <memory>
class ICM42670;
class ICM42607PSensor : public MotionSensor
{
private:
std::unique_ptr<ICM42670> sensor;
TwoWire *wire = nullptr;
public:
explicit ICM42607PSensor(ScanI2C::FoundDevice foundDevice);
~ICM42607PSensor() override;
virtual bool init() override;
virtual int32_t runOnce() override;
};
#endif
#endif
-115
View File
@@ -1,115 +0,0 @@
#include "MMC5983MASensor.h"
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include(<SparkFun_MMC5983MA_Arduino_Library.h>)
#include "detect/ScanI2CTwoWire.h"
#if !defined(MESHTASTIC_EXCLUDE_SCREEN)
extern graphics::Screen *screen;
#endif
static constexpr float MMC5983MA_ZERO_FIELD = 131072.0f;
static constexpr float MMC5983MA_COUNTS_PER_GAUSS = 16384.0f;
static constexpr uint16_t MMC5983MA_CONTINUOUS_FREQUENCY_HZ = 10;
static constexpr float MMC5983MA_HEADING_OFFSET_DEG = 180.0f;
MMC5983MASensor::MMC5983MASensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {}
bool MMC5983MASensor::init()
{
LOG_DEBUG("MMC5983MA begin on addr 0x%02X (port=%d)", device.address.address, device.address.port);
TwoWire *wire = ScanI2CTwoWire::fetchI2CBus(device.address);
if (!sensor.begin(*wire)) {
LOG_DEBUG("MMC5983MA init error");
return false;
}
sensor.softReset();
sensor.setFilterBandwidth(100);
sensor.performSetOperation();
sensor.enableAutomaticSetReset();
continuousMode = sensor.setContinuousModeFrequency(MMC5983MA_CONTINUOUS_FREQUENCY_HZ);
continuousMode &= sensor.enableContinuousMode();
if (!continuousMode) {
LOG_DEBUG("MMC5983MA continuous mode failed, using single-shot reads");
sensor.disableContinuousMode();
}
loadMagnetometerCalibration(compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
return true;
}
bool MMC5983MASensor::readMagnetometer(float &xGauss, float &yGauss, float &zGauss)
{
uint32_t rawX = 0;
uint32_t rawY = 0;
uint32_t rawZ = 0;
if (!(continuousMode ? sensor.readFieldsXYZ(&rawX, &rawY, &rawZ) : sensor.getMeasurementXYZ(&rawX, &rawY, &rawZ))) {
LOG_DEBUG("MMC5983MA read failed");
return false;
}
xGauss = ((float)rawX - MMC5983MA_ZERO_FIELD) / MMC5983MA_COUNTS_PER_GAUSS;
yGauss = ((float)rawY - MMC5983MA_ZERO_FIELD) / MMC5983MA_COUNTS_PER_GAUSS;
zGauss = ((float)rawZ - MMC5983MA_ZERO_FIELD) / MMC5983MA_COUNTS_PER_GAUSS;
return true;
}
int32_t MMC5983MASensor::runOnce()
{
float magX = 0, magY = 0, magZ = 0;
if (!readMagnetometer(magX, magY, magZ)) {
return MOTION_SENSOR_CHECK_INTERVAL_MS;
}
#if !defined(MESHTASTIC_EXCLUDE_SCREEN)
if (doCalibration) {
beginCalibrationDisplay(showingScreen);
updateCalibrationExtrema(magX, magY, magZ, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
finishCalibrationIfExpired(showingScreen, compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ,
lowestZ);
}
#endif
magX -= (highestX + lowestX) / 2;
magY -= (highestY + lowestY) / 2;
magZ -= (highestZ + lowestZ) / 2;
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
float heading = atan2f(magY, magX) * RAD_TO_DEG + MMC5983MA_HEADING_OFFSET_DEG;
if (heading < 0.0f) {
heading += 360.0f;
} else if (heading >= 360.0f) {
heading -= 360.0f;
}
heading = applyCompassOrientation(heading);
if (screen) {
screen->setHeading(heading);
}
#endif
return MOTION_SENSOR_CHECK_INTERVAL_MS;
}
void MMC5983MASensor::calibrate(uint16_t forSeconds)
{
#if !defined(MESHTASTIC_EXCLUDE_SCREEN)
float xGauss = 0.0f;
float yGauss = 0.0f;
float zGauss = 0.0f;
LOG_DEBUG("MMC5983MA calibration started for %is", forSeconds);
if (readMagnetometer(xGauss, yGauss, zGauss)) {
seedCalibrationExtrema(xGauss, yGauss, zGauss, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
} else {
seedCalibrationExtrema(0.0f, 0.0f, 0.0f, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
}
startCalibrationWindow(forSeconds);
#endif
}
#endif
-31
View File
@@ -1,31 +0,0 @@
#pragma once
#ifndef _MMC5983MA_SENSOR_H_
#define _MMC5983MA_SENSOR_H_
#include "MotionSensor.h"
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include(<SparkFun_MMC5983MA_Arduino_Library.h>)
#include <SparkFun_MMC5983MA_Arduino_Library.h>
class MMC5983MASensor : public MotionSensor
{
private:
SFE_MMC5983MA sensor;
bool continuousMode = false;
bool showingScreen = false;
static constexpr const char *compassCalibrationFileName = "/prefs/compass_mmc5983ma.dat";
float highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0;
bool readMagnetometer(float &xGauss, float &yGauss, float &zGauss);
public:
explicit MMC5983MASensor(ScanI2C::FoundDevice foundDevice);
virtual bool init() override;
virtual int32_t runOnce() override;
virtual void calibrate(uint16_t forSeconds) override;
};
#endif
#endif
-115
View File
@@ -1,115 +0,0 @@
#pragma once
#ifndef _MAGNETOMETER_THREAD_H_
#define _MAGNETOMETER_THREAD_H_
#include "configuration.h"
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_MAGNETOMETER
#include "../concurrency/OSThread.h"
#include "MMC5983MASensor.h"
#include "MotionSensor.h"
extern ScanI2C::DeviceAddress magnetometer_found;
class MagnetometerThread : public concurrency::OSThread
{
private:
MotionSensor *sensor = nullptr;
ScanI2C::FoundDevice device;
bool isInitialised = false;
public:
explicit MagnetometerThread(ScanI2C::FoundDevice foundDevice) : OSThread("Magnetometer")
{
device = foundDevice;
init();
}
explicit MagnetometerThread(ScanI2C::DeviceType type) : MagnetometerThread(ScanI2C::FoundDevice{type, magnetometer_found}) {}
void start()
{
init();
setIntervalFromNow(0);
};
void calibrate(uint16_t forSeconds)
{
if (sensor) {
sensor->calibrate(forSeconds);
setIntervalFromNow(0);
}
}
protected:
int32_t runOnce() override
{
canSleep = true;
if (isInitialised) {
return sensor->runOnce();
}
return MOTION_SENSOR_CHECK_INTERVAL_MS;
}
private:
void init()
{
if (isInitialised) {
return;
}
if (device.address.port == ScanI2C::I2CPort::NO_I2C || device.address.address == 0 || device.type == ScanI2C::NONE) {
LOG_DEBUG("MagnetometerThread Disable due to no sensors found");
disable();
return;
}
switch (device.type) {
case ScanI2C::DeviceType::MMC5983MA:
sensor = new MMC5983MASensor(device);
break;
default:
disable();
return;
}
isInitialised = sensor->init();
if (!isInitialised) {
clean();
}
LOG_DEBUG("MagnetometerThread::init %s", isInitialised ? "ok" : "failed");
}
MagnetometerThread(const MagnetometerThread &other) : OSThread::OSThread("Magnetometer") { this->copy(other); }
virtual ~MagnetometerThread() { clean(); }
MagnetometerThread &operator=(const MagnetometerThread &other)
{
this->copy(other);
return *this;
}
void copy(const MagnetometerThread &other)
{
if (this != &other) {
clean();
this->device = ScanI2C::FoundDevice(other.device.type,
ScanI2C::DeviceAddress(other.device.address.port, other.device.address.address));
}
}
void clean()
{
isInitialised = false;
delete sensor;
sensor = nullptr;
}
};
#endif
#endif
+170 -4
View File
@@ -23,6 +23,10 @@
#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>
@@ -143,6 +147,96 @@ 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)
{
@@ -292,6 +386,26 @@ 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);
}
@@ -319,10 +433,12 @@ 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;
}
@@ -350,7 +466,7 @@ MQTT::MQTT() : concurrency::OSThread("mqtt"), mqttQueue(MAX_MQTT_QUEUE)
enabled = true;
runASAP = true;
reconnectCount = 0;
#ifndef PIO_UNIT_TESTING
#if !IS_RUNNING_TESTS
publishNodeInfo();
#endif
}
@@ -473,6 +589,14 @@ 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
@@ -550,7 +674,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);
#ifndef PIO_UNIT_TESTING
#if !IS_RUNNING_TESTS
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
if (cn) {
cn->level = meshtastic_LogRecord_Level_WARNING;
@@ -566,7 +690,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);
#ifndef PIO_UNIT_TESTING
#if !IS_RUNNING_TESTS
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
cn->level = meshtastic_LogRecord_Level_ERROR;
cn->time = getValidTime(RTCQualityFromNet);
@@ -582,7 +706,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);
#ifndef PIO_UNIT_TESTING
#if !IS_RUNNING_TESTS
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
cn->level = meshtastic_LogRecord_Level_ERROR;
cn->time = getValidTime(RTCQualityFromNet);
@@ -611,6 +735,33 @@ 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)
@@ -674,6 +825,21 @@ 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;
+6 -2
View File
@@ -6,6 +6,9 @@
#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>)
@@ -98,8 +101,9 @@ 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 mapTopic = "/2/map/"; // For protobuf-encoded MapReport messages
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
// For map reporting (only applies when enabled)
const uint32_t default_map_position_precision = 14; // defaults to max. offset of ~1459m
+19 -27
View File
@@ -1,5 +1,5 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_BLUETOOTH && !defined(CONFIG_IDF_TARGET_ESP32P4)
#if !MESHTASTIC_EXCLUDE_BLUETOOTH
#include "BluetoothCommon.h"
#include "NimbleBluetooth.h"
#include "PowerFSM.h"
@@ -25,9 +25,6 @@
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;
@@ -190,9 +187,11 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
LOG_INFO("BLE onConfigStart");
// Prefer high throughput during config/setup, at the cost of high power consumption (for a few seconds)
uint16_t conn_handle = nimbleBluetoothConnHandle.load();
if (conn_handle != BLE_HS_CONN_HANDLE_NONE) {
requestHighThroughputConnection(conn_handle);
if (bleServer && isConnected()) {
uint16_t conn_handle = nimbleBluetoothConnHandle.load();
if (conn_handle != BLE_HS_CONN_HANDLE_NONE) {
requestHighThroughputConnection(conn_handle);
}
}
}
@@ -201,9 +200,11 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
LOG_INFO("BLE onConfigComplete");
// Switch to lower power consumption BLE connection params for steady-state use after config/setup is complete
uint16_t conn_handle = nimbleBluetoothConnHandle.load();
if (conn_handle != BLE_HS_CONN_HANDLE_NONE) {
requestLowerPowerConnection(conn_handle);
if (bleServer && isConnected()) {
uint16_t conn_handle = nimbleBluetoothConnHandle.load();
if (conn_handle != BLE_HS_CONN_HANDLE_NONE) {
requestLowerPowerConnection(conn_handle);
}
}
}
@@ -336,7 +337,7 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
}
/// Check the current underlying physical link to see if the client is currently connected
virtual bool checkIsConnected() override { return nimbleBluetoothConnHandle.load() != BLE_HS_CONN_HANDLE_NONE; }
virtual bool checkIsConnected() override { return bleServer && bleServer->getConnectedCount() > 0; }
void requestHighThroughputConnection(uint16_t conn_handle)
{
@@ -571,7 +572,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) && !defined(M5STACK_UNITC6L)
#if !defined(OLED_TINY)
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");
@@ -731,14 +732,13 @@ bool NimbleBluetooth::isActive()
bool NimbleBluetooth::isConnected()
{
return nimbleBluetoothConnHandle.load() != BLE_HS_CONN_HANDLE_NONE;
return bleServer && bleServer->getConnectedCount() > 0;
}
int NimbleBluetooth::getRssi()
{
#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6)
uint16_t conn_handle = nimbleBluetoothConnHandle.load();
if (conn_handle != BLE_HS_CONN_HANDLE_NONE) {
if (!bleServer || !isConnected()) {
return 0; // No active BLE connection
}
@@ -755,8 +755,9 @@ int NimbleBluetooth::getRssi()
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);
const int rc = ble_gap_conn_rssi(connHandle, &rssi);
if (rc == 0) {
return rssi;
@@ -878,7 +879,7 @@ void NimbleBluetooth::setupService()
/// Given a level between 0-100, update the BLE attribute
void updateBatteryLevel(uint8_t level)
{
if ((config.bluetooth.enabled == true) && BatteryCharacteristic && nimbleBluetooth && nimbleBluetooth->isConnected()) {
if ((config.bluetooth.enabled == true) && bleServer && nimbleBluetooth->isConnected()) {
BatteryCharacteristic->setValue(&level, 1);
BatteryCharacteristic->notify();
}
@@ -893,7 +894,7 @@ void NimbleBluetooth::clearBonds()
void NimbleBluetooth::sendLog(const uint8_t *logMessage, size_t length)
{
if (!isConnected() || length > 512) {
if (!bleServer || !isConnected() || length > 512) {
return;
}
logRadioCharacteristic->setValue(logMessage, length);
@@ -908,13 +909,4 @@ void clearNVS()
ESP.restart();
#endif
}
#else
void updateBatteryLevel(uint8_t level)
{
(void)level;
}
void clearNVS() {}
#endif
+9 -10
View File
@@ -1,19 +1,18 @@
#pragma once
#include "BluetoothCommon.h"
class NimbleBluetooth : public BluetoothApi
class NimbleBluetooth : BluetoothApi
{
public:
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 setup();
void shutdown();
void deinit();
void clearBonds();
bool isActive();
bool isConnected();
int getRssi();
void sendLog(const uint8_t *logMessage, size_t length);
void startAdvertising();
virtual ~NimbleBluetooth() {}
bool isDeInit = false;
private:
-7
View File
@@ -1,7 +0,0 @@
#pragma once
#if __has_include_next("esp_eth_driver.h")
#include_next "esp_eth_driver.h"
#elif __has_include("esp_eth.h")
#include "esp_eth.h"
#endif
+6 -24
View File
@@ -4,13 +4,9 @@
#include "esp_task_wdt.h"
#include "main.h"
#if !MESHTASTIC_EXCLUDE_BLUETOOTH
#if defined(CONFIG_IDF_TARGET_ESP32P4)
#include "bluetooth/HostedBluetooth.h"
#elif !defined(CONFIG_IDF_TARGET_ESP32S2)
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !MESHTASTIC_EXCLUDE_BLUETOOTH
#include "nimble/NimbleBluetooth.h"
#endif
#endif
#include <MeshtasticOTA.h>
@@ -44,30 +40,16 @@ void setBluetoothEnable(bool enable)
if (config.bluetooth.enabled == true)
#endif
{
#if defined(CONFIG_IDF_TARGET_ESP32P4)
if (!enable) {
if (bluetoothApi && bluetoothApi->isActive()) {
bluetoothApi->shutdown();
powerMon->clearState(meshtastic_PowerMon_State_BT_On);
}
return;
if (!nimbleBluetooth) {
nimbleBluetooth = new NimbleBluetooth();
}
#endif
if (!bluetoothApi) {
#if defined(CONFIG_IDF_TARGET_ESP32P4)
bluetoothApi = new HostedBluetooth();
#else
bluetoothApi = new NimbleBluetooth();
#endif
}
if (enable && !bluetoothApi->isActive()) {
if (enable && !nimbleBluetooth->isActive()) {
powerMon->setState(meshtastic_PowerMon_State_BT_On);
bluetoothApi->setup();
nimbleBluetooth->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 bluetoothApi->deinit(). Requires reboot to reverse
// For deep-sleep, shutdown hardware with nimbleBluetooth->deinit(). Requires reboot to reverse
}
}
#else
-2
View File
@@ -117,8 +117,6 @@
#define HW_VENDOR meshtastic_HardwareModel_PRIVATE_HW
#elif defined(HELTEC_T114)
#define HW_VENDOR meshtastic_HardwareModel_HELTEC_MESH_NODE_T114
#elif defined(HELTEC_MESH_NODE_T1)
#define HW_VENDOR meshtastic_HardwareModel_HELTEC_MESH_NODE_T1
#elif defined(MESHLINK)
#define HW_VENDOR meshtastic_HardwareModel_MESHLINK
#elif defined(SEEED_XIAO_NRF52840_KIT)
+1 -1
View File
@@ -562,7 +562,7 @@ void portduinoSetup()
}
getMacAddr(dmac);
#ifndef PIO_UNIT_TESTING
#ifndef UNIT_TEST
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;
+245
View File
@@ -0,0 +1,245 @@
/*
* 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;
}
+73
View File
@@ -0,0 +1,73 @@
/*
* 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
+897
View File
@@ -0,0 +1,897 @@
/*
* 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;
}
+95
View File
@@ -0,0 +1,95 @@
/*
* 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
+174 -168
View File
@@ -1,12 +1,11 @@
#if ARCH_PORTDUINO
#ifndef NRF52_USE_JSON
#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"
@@ -16,49 +15,41 @@
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;
Json::Value jsonObj(Json::objectValue);
JSONObject jsonObj;
if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
Json::Value msgPayload(Json::objectValue);
JSONObject msgPayload;
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;
Json::Value parsed;
if (tryParseJson(payloadStr, parsed)) {
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) {
if (shouldLog)
LOG_INFO("text message payload is of type json");
jsonObj["payload"] = parsed;
// if it is, then we can just use the json object
jsonObj["payload"] = json_value;
} 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"] = payloadStr;
jsonObj["payload"] = msgPayload;
msgPayload["text"] = new JSONValue(payloadStr);
jsonObj["payload"] = new JSONValue(msgPayload);
}
break;
}
@@ -70,129 +61,133 @@ 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"] = (int)decoded->variant.device_metrics.battery_level;
msgPayload["battery_level"] = new JSONValue((int)decoded->variant.device_metrics.battery_level);
}
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;
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);
} 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"] = decoded->variant.environment_metrics.temperature;
msgPayload["temperature"] = new JSONValue(decoded->variant.environment_metrics.temperature);
}
if (decoded->variant.environment_metrics.has_relative_humidity) {
msgPayload["relative_humidity"] = decoded->variant.environment_metrics.relative_humidity;
msgPayload["relative_humidity"] = new JSONValue(decoded->variant.environment_metrics.relative_humidity);
}
if (decoded->variant.environment_metrics.has_barometric_pressure) {
msgPayload["barometric_pressure"] = decoded->variant.environment_metrics.barometric_pressure;
msgPayload["barometric_pressure"] =
new JSONValue(decoded->variant.environment_metrics.barometric_pressure);
}
if (decoded->variant.environment_metrics.has_gas_resistance) {
msgPayload["gas_resistance"] = decoded->variant.environment_metrics.gas_resistance;
msgPayload["gas_resistance"] = new JSONValue(decoded->variant.environment_metrics.gas_resistance);
}
if (decoded->variant.environment_metrics.has_voltage) {
msgPayload["voltage"] = decoded->variant.environment_metrics.voltage;
msgPayload["voltage"] = new JSONValue(decoded->variant.environment_metrics.voltage);
}
if (decoded->variant.environment_metrics.has_current) {
msgPayload["current"] = decoded->variant.environment_metrics.current;
msgPayload["current"] = new JSONValue(decoded->variant.environment_metrics.current);
}
if (decoded->variant.environment_metrics.has_lux) {
msgPayload["lux"] = decoded->variant.environment_metrics.lux;
msgPayload["lux"] = new JSONValue(decoded->variant.environment_metrics.lux);
}
if (decoded->variant.environment_metrics.has_white_lux) {
msgPayload["white_lux"] = decoded->variant.environment_metrics.white_lux;
msgPayload["white_lux"] = new JSONValue(decoded->variant.environment_metrics.white_lux);
}
if (decoded->variant.environment_metrics.has_iaq) {
msgPayload["iaq"] = (Json::UInt)decoded->variant.environment_metrics.iaq;
msgPayload["iaq"] = new JSONValue((uint)decoded->variant.environment_metrics.iaq);
}
if (decoded->variant.environment_metrics.has_distance) {
msgPayload["distance"] = decoded->variant.environment_metrics.distance;
msgPayload["distance"] = new JSONValue(decoded->variant.environment_metrics.distance);
}
if (decoded->variant.environment_metrics.has_wind_speed) {
msgPayload["wind_speed"] = decoded->variant.environment_metrics.wind_speed;
msgPayload["wind_speed"] = new JSONValue(decoded->variant.environment_metrics.wind_speed);
}
if (decoded->variant.environment_metrics.has_wind_direction) {
msgPayload["wind_direction"] = (Json::UInt)decoded->variant.environment_metrics.wind_direction;
msgPayload["wind_direction"] = new JSONValue((uint)decoded->variant.environment_metrics.wind_direction);
}
if (decoded->variant.environment_metrics.has_wind_gust) {
msgPayload["wind_gust"] = decoded->variant.environment_metrics.wind_gust;
msgPayload["wind_gust"] = new JSONValue(decoded->variant.environment_metrics.wind_gust);
}
if (decoded->variant.environment_metrics.has_wind_lull) {
msgPayload["wind_lull"] = decoded->variant.environment_metrics.wind_lull;
msgPayload["wind_lull"] = new JSONValue(decoded->variant.environment_metrics.wind_lull);
}
if (decoded->variant.environment_metrics.has_radiation) {
msgPayload["radiation"] = decoded->variant.environment_metrics.radiation;
msgPayload["radiation"] = new JSONValue(decoded->variant.environment_metrics.radiation);
}
if (decoded->variant.environment_metrics.has_ir_lux) {
msgPayload["ir_lux"] = decoded->variant.environment_metrics.ir_lux;
msgPayload["ir_lux"] = new JSONValue(decoded->variant.environment_metrics.ir_lux);
}
if (decoded->variant.environment_metrics.has_uv_lux) {
msgPayload["uv_lux"] = decoded->variant.environment_metrics.uv_lux;
msgPayload["uv_lux"] = new JSONValue(decoded->variant.environment_metrics.uv_lux);
}
if (decoded->variant.environment_metrics.has_weight) {
msgPayload["weight"] = decoded->variant.environment_metrics.weight;
msgPayload["weight"] = new JSONValue(decoded->variant.environment_metrics.weight);
}
if (decoded->variant.environment_metrics.has_rainfall_1h) {
msgPayload["rainfall_1h"] = decoded->variant.environment_metrics.rainfall_1h;
msgPayload["rainfall_1h"] = new JSONValue(decoded->variant.environment_metrics.rainfall_1h);
}
if (decoded->variant.environment_metrics.has_rainfall_24h) {
msgPayload["rainfall_24h"] = decoded->variant.environment_metrics.rainfall_24h;
msgPayload["rainfall_24h"] = new JSONValue(decoded->variant.environment_metrics.rainfall_24h);
}
if (decoded->variant.environment_metrics.has_soil_moisture) {
msgPayload["soil_moisture"] = (Json::UInt)decoded->variant.environment_metrics.soil_moisture;
msgPayload["soil_moisture"] = new JSONValue((uint)decoded->variant.environment_metrics.soil_moisture);
}
if (decoded->variant.environment_metrics.has_soil_temperature) {
msgPayload["soil_temperature"] = decoded->variant.environment_metrics.soil_temperature;
msgPayload["soil_temperature"] = new JSONValue(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"] = (Json::UInt)decoded->variant.air_quality_metrics.pm10_standard;
msgPayload["pm10"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_standard);
}
if (decoded->variant.air_quality_metrics.has_pm25_standard) {
msgPayload["pm25"] = (Json::UInt)decoded->variant.air_quality_metrics.pm25_standard;
msgPayload["pm25"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_standard);
}
if (decoded->variant.air_quality_metrics.has_pm100_standard) {
msgPayload["pm100"] = (Json::UInt)decoded->variant.air_quality_metrics.pm100_standard;
msgPayload["pm100"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_standard);
}
if (decoded->variant.air_quality_metrics.has_co2) {
msgPayload["co2"] = (Json::UInt)decoded->variant.air_quality_metrics.co2;
msgPayload["co2"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.co2);
}
if (decoded->variant.air_quality_metrics.has_co2_temperature) {
msgPayload["co2_temperature"] = decoded->variant.air_quality_metrics.co2_temperature;
msgPayload["co2_temperature"] = new JSONValue(decoded->variant.air_quality_metrics.co2_temperature);
}
if (decoded->variant.air_quality_metrics.has_co2_humidity) {
msgPayload["co2_humidity"] = decoded->variant.air_quality_metrics.co2_humidity;
msgPayload["co2_humidity"] = new JSONValue(decoded->variant.air_quality_metrics.co2_humidity);
}
if (decoded->variant.air_quality_metrics.has_form_formaldehyde) {
msgPayload["form_formaldehyde"] = decoded->variant.air_quality_metrics.form_formaldehyde;
msgPayload["form_formaldehyde"] = new JSONValue(decoded->variant.air_quality_metrics.form_formaldehyde);
}
if (decoded->variant.air_quality_metrics.has_form_temperature) {
msgPayload["form_temperature"] = decoded->variant.air_quality_metrics.form_temperature;
msgPayload["form_temperature"] = new JSONValue(decoded->variant.air_quality_metrics.form_temperature);
}
if (decoded->variant.air_quality_metrics.has_form_humidity) {
msgPayload["form_humidity"] = decoded->variant.air_quality_metrics.form_humidity;
msgPayload["form_humidity"] = new JSONValue(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"] = decoded->variant.power_metrics.ch1_voltage;
msgPayload["voltage_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_voltage);
}
if (decoded->variant.power_metrics.has_ch1_current) {
msgPayload["current_ch1"] = decoded->variant.power_metrics.ch1_current;
msgPayload["current_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_current);
}
if (decoded->variant.power_metrics.has_ch2_voltage) {
msgPayload["voltage_ch2"] = decoded->variant.power_metrics.ch2_voltage;
msgPayload["voltage_ch2"] = new JSONValue(decoded->variant.power_metrics.ch2_voltage);
}
if (decoded->variant.power_metrics.has_ch2_current) {
msgPayload["current_ch2"] = decoded->variant.power_metrics.ch2_current;
msgPayload["current_ch2"] = new JSONValue(decoded->variant.power_metrics.ch2_current);
}
if (decoded->variant.power_metrics.has_ch3_voltage) {
msgPayload["voltage_ch3"] = decoded->variant.power_metrics.ch3_voltage;
msgPayload["voltage_ch3"] = new JSONValue(decoded->variant.power_metrics.ch3_voltage);
}
if (decoded->variant.power_metrics.has_ch3_current) {
msgPayload["current_ch3"] = decoded->variant.power_metrics.ch3_current;
msgPayload["current_ch3"] = new JSONValue(decoded->variant.power_metrics.ch3_current);
}
}
jsonObj["payload"] = msgPayload;
jsonObj["payload"] = new JSONValue(msgPayload);
} else if (shouldLog) {
LOG_ERROR(errStr, msgType.c_str());
}
@@ -205,12 +200,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"] = 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;
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);
} else if (shouldLog) {
LOG_ERROR(errStr, msgType.c_str());
}
@@ -224,38 +219,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"] = (Json::UInt)decoded->time;
msgPayload["time"] = new JSONValue((unsigned int)decoded->time);
}
if ((int)decoded->timestamp) {
msgPayload["timestamp"] = (Json::UInt)decoded->timestamp;
msgPayload["timestamp"] = new JSONValue((unsigned int)decoded->timestamp);
}
msgPayload["latitude_i"] = (int)decoded->latitude_i;
msgPayload["longitude_i"] = (int)decoded->longitude_i;
msgPayload["latitude_i"] = new JSONValue((int)decoded->latitude_i);
msgPayload["longitude_i"] = new JSONValue((int)decoded->longitude_i);
if ((int)decoded->altitude) {
msgPayload["altitude"] = (int)decoded->altitude;
msgPayload["altitude"] = new JSONValue((int)decoded->altitude);
}
if ((int)decoded->ground_speed) {
msgPayload["ground_speed"] = (Json::UInt)decoded->ground_speed;
msgPayload["ground_speed"] = new JSONValue((unsigned int)decoded->ground_speed);
}
if (int(decoded->ground_track)) {
msgPayload["ground_track"] = (Json::UInt)decoded->ground_track;
msgPayload["ground_track"] = new JSONValue((unsigned int)decoded->ground_track);
}
if (int(decoded->sats_in_view)) {
msgPayload["sats_in_view"] = (Json::UInt)decoded->sats_in_view;
msgPayload["sats_in_view"] = new JSONValue((unsigned int)decoded->sats_in_view);
}
if ((int)decoded->PDOP) {
msgPayload["PDOP"] = (int)decoded->PDOP;
msgPayload["PDOP"] = new JSONValue((int)decoded->PDOP);
}
if ((int)decoded->HDOP) {
msgPayload["HDOP"] = (int)decoded->HDOP;
msgPayload["HDOP"] = new JSONValue((int)decoded->HDOP);
}
if ((int)decoded->VDOP) {
msgPayload["VDOP"] = (int)decoded->VDOP;
msgPayload["VDOP"] = new JSONValue((int)decoded->VDOP);
}
if ((int)decoded->precision_bits) {
msgPayload["precision_bits"] = (int)decoded->precision_bits;
msgPayload["precision_bits"] = new JSONValue((int)decoded->precision_bits);
}
jsonObj["payload"] = msgPayload;
jsonObj["payload"] = new JSONValue(msgPayload);
} else if (shouldLog) {
LOG_ERROR(errStr, msgType.c_str());
}
@@ -268,14 +263,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"] = (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;
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);
} else if (shouldLog) {
LOG_ERROR(errStr, msgType.c_str());
}
@@ -289,26 +284,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"] = (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);
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;
for (uint8_t i = 0; i < decoded->neighbors_count; i++) {
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);
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));
}
msgPayload["neighbors"] = neighbors;
jsonObj["payload"] = msgPayload;
msgPayload["neighbors"] = new JSONValue(neighbors);
jsonObj["payload"] = new JSONValue(msgPayload);
} else if (shouldLog) {
LOG_ERROR(errStr, msgType.c_str());
}
break;
}
case meshtastic_PortNum_TRACEROUTE_APP: {
if (mp->decoded.request_id) {
if (mp->decoded.request_id) { // Only report the traceroute response
msgType = "traceroute";
meshtastic_RouteDiscovery scratch;
meshtastic_RouteDiscovery *decoded = NULL;
@@ -316,12 +311,13 @@ 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;
Json::Value route(Json::arrayValue);
Json::Value routeBack(Json::arrayValue);
Json::Value snrTowards(Json::arrayValue);
Json::Value snrBack(Json::arrayValue);
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
auto addToRoute = [](Json::Value *r, NodeNum num) {
// Lambda function for adding a long name to the route
auto addToRoute = [](JSONArray *route, NodeNum num) {
char long_name[40] = "Unknown";
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num);
bool name_known = nodeInfoLiteHasUser(node);
@@ -331,32 +327,33 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
memcpy(long_name, node->long_name, copy_len);
long_name[copy_len] = '\0';
}
r->append(long_name);
route->push_back(new JSONValue(long_name));
};
addToRoute(&route, mp->to);
addToRoute(&route, mp->to); // Started at the original transmitter (destination of response)
for (uint8_t i = 0; i < decoded->route_count; i++) {
addToRoute(&route, decoded->route[i]);
}
addToRoute(&route, mp->from);
addToRoute(&route, mp->from); // Ended at the original destination (source of response)
addToRoute(&routeBack, mp->from);
addToRoute(&routeBack, mp->from); // Started at the original destination (source of response)
for (uint8_t i = 0; i < decoded->route_back_count; i++) {
addToRoute(&routeBack, decoded->route_back[i]);
}
addToRoute(&routeBack, mp->to);
addToRoute(&routeBack, mp->to); // Ended at the original transmitter (destination of response)
for (uint8_t i = 0; i < decoded->snr_back_count; i++) {
snrBack.append((float)decoded->snr_back[i] / 4);
}
for (uint8_t i = 0; i < decoded->snr_towards_count; i++) {
snrTowards.append((float)decoded->snr_towards[i] / 4);
snrBack.push_back(new JSONValue((float)decoded->snr_back[i] / 4));
}
msgPayload["route"] = route;
msgPayload["route_back"] = routeBack;
msgPayload["snr_back"] = snrBack;
msgPayload["snr_towards"] = snrTowards;
jsonObj["payload"] = msgPayload;
for (uint8_t i = 0; i < decoded->snr_towards_count; i++) {
snrTowards.push_back(new JSONValue((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);
} else if (shouldLog) {
LOG_ERROR(errStr, msgType.c_str());
}
@@ -367,9 +364,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;
msgPayload["text"] = payloadStr;
jsonObj["payload"] = msgPayload;
payloadStr[mp->decoded.payload.size] = 0; // null terminated string
msgPayload["text"] = new JSONValue(payloadStr);
jsonObj["payload"] = new JSONValue(msgPayload);
break;
}
#ifdef ARCH_ESP32
@@ -380,10 +377,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"] = (Json::UInt)decoded->wifi;
msgPayload["ble_count"] = (Json::UInt)decoded->ble;
msgPayload["uptime"] = (Json::UInt)decoded->uptime;
jsonObj["payload"] = msgPayload;
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);
} else if (shouldLog) {
LOG_ERROR(errStr, msgType.c_str());
}
@@ -399,19 +396,20 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
decoded = &scratch;
if (decoded->type == meshtastic_HardwareMessage_Type_GPIOS_CHANGED) {
msgType = "gpios_changed";
msgPayload["gpio_value"] = (Json::UInt)decoded->gpio_value;
jsonObj["payload"] = msgPayload;
msgPayload["gpio_value"] = new JSONValue((unsigned int)decoded->gpio_value);
jsonObj["payload"] = new JSONValue(msgPayload);
} else if (decoded->type == meshtastic_HardwareMessage_Type_READ_GPIOS_REPLY) {
msgType = "gpios_read_reply";
msgPayload["gpio_value"] = (Json::UInt)decoded->gpio_value;
msgPayload["gpio_mask"] = (Json::UInt)decoded->gpio_mask;
jsonObj["payload"] = msgPayload;
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);
}
} else if (shouldLog) {
LOG_ERROR(errStr, "RemoteHardware");
}
break;
}
// add more packet types here if needed
default:
break;
}
@@ -419,56 +417,64 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
LOG_WARN("Couldn't convert encrypted payload of MeshPacket to JSON");
}
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();
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());
if (mp->rx_rssi != 0)
jsonObj["rssi"] = (int)mp->rx_rssi;
jsonObj["rssi"] = new JSONValue((int)mp->rx_rssi);
if (mp->rx_snr != 0)
jsonObj["snr"] = (float)mp->rx_snr;
jsonObj["snr"] = new JSONValue((float)mp->rx_snr);
const int8_t hopsAway = getHopsAway(*mp);
if (hopsAway >= 0) {
jsonObj["hops_away"] = (Json::UInt)(hopsAway);
jsonObj["hop_start"] = (Json::UInt)(mp->hop_start);
jsonObj["hops_away"] = new JSONValue((unsigned int)(hopsAway));
jsonObj["hop_start"] = new JSONValue((unsigned int)(mp->hop_start));
}
std::string jsonStr = writeCompact(jsonObj);
// serialize and write it to the stream
JSONValue *value = new JSONValue(jsonObj);
std::string jsonStr = value->Stringify();
if (shouldLog)
LOG_INFO("serialized json message: %s", jsonStr.c_str());
delete value;
return jsonStr;
}
std::string MeshPacketSerializer::JsonSerializeEncrypted(const meshtastic_MeshPacket *mp)
{
Json::Value jsonObj(Json::objectValue);
JSONObject jsonObj;
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;
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);
if (mp->rx_rssi != 0)
jsonObj["rssi"] = (int)mp->rx_rssi;
jsonObj["rssi"] = new JSONValue((int)mp->rx_rssi);
if (mp->rx_snr != 0)
jsonObj["snr"] = (float)mp->rx_snr;
jsonObj["snr"] = new JSONValue((float)mp->rx_snr);
const int8_t hopsAway = getHopsAway(*mp);
if (hopsAway >= 0) {
jsonObj["hops_away"] = (Json::UInt)(hopsAway);
jsonObj["hop_start"] = (Json::UInt)(mp->hop_start);
jsonObj["hops_away"] = new JSONValue((unsigned int)(hopsAway));
jsonObj["hop_start"] = new JSONValue((unsigned int)(mp->hop_start));
}
jsonObj["size"] = (Json::UInt)mp->encrypted.size;
jsonObj["size"] = new JSONValue((unsigned int)mp->encrypted.size);
auto encryptedStr = bytesToHex(mp->encrypted.bytes, mp->encrypted.size);
jsonObj["bytes"] = encryptedStr;
jsonObj["bytes"] = new JSONValue(encryptedStr.c_str());
return writeCompact(jsonObj);
// serialize and write it to the stream
JSONValue *value = new JSONValue(jsonObj);
std::string jsonStr = value->Stringify();
delete value;
return jsonStr;
}
#endif
@@ -0,0 +1,425 @@
#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
View File
@@ -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 (bluetoothApi)
bluetoothApi->deinit();
if (nimbleBluetooth)
nimbleBluetooth->deinit();
#endif
#ifdef ARCH_ESP32
+15 -89
View File
@@ -15,38 +15,6 @@ 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.
@@ -114,15 +82,12 @@ 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> // required for printf() — used for blank-line group separators
#include <cstdio>
#include <cstring>
#include <memory>
// --- Test output helpers ---
// 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.
// Unity swallows printf/stdout. Only TEST_MESSAGE() output appears in results.
#define MSG_BUF_LEN 200
#define TEST_MSG_FMT(fmt, ...) do { \
char _buf[MSG_BUF_LEN]; \
@@ -147,9 +112,6 @@ 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
}
@@ -200,7 +162,7 @@ class MockNodeDB : public NodeDB
node.num = num;
node.has_hops_away = hasHops;
node.hops_away = hopsAway;
nodeInfoLiteSetBit(&node, NODEINFO_BITFIELD_VIA_MQTT_MASK, viaMqtt);
node.via_mqtt = viaMqtt;
node.last_heard = getTime() - ageSecs;
testNodes.push_back(node);
meshNodes = &testNodes;
@@ -223,34 +185,20 @@ Subclass the module under test to make protected methods callable and private me
class YourModuleTestShim : public YourModule
{
public:
// 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.
// Expose protected methods
using YourModule::runOnce;
using YourModule::someProtectedMethod;
// Wrap private members with setter methods (friend grants direct access here).
// Access private members via friend (see below)
void setPrivateField(int x) { privateField = x; }
};
```
For methods you want to expose via `using`, use the conditional access-specifier pattern in the header — **not** plain `friend`:
In the module header, grant friend access under the `UNIT_TEST` define (set automatically by PlatformIO's test framework):
```cpp
// In YourModule.h, inside the class body:
#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
#ifdef UNIT_TEST
friend class YourModuleTestShim;
#endif
```
@@ -336,21 +284,6 @@ 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)
@@ -358,7 +291,6 @@ PlatformIO defines `PIO_UNIT_TESTING` during `pio test` builds. Several producti
- [ ] 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
@@ -376,21 +308,15 @@ 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_default` | Default configuration helpers |
| `test_hop_scaling` | Hop scaling algorithm |
| `test_http_content_handler` | HTTP handling |
| `test_mac_from_string` | MAC address parsing |
| `test_mqtt` | MQTT integration |
| `test_radio` | Radio interface |
| `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 conversions |
| `test_utf8` | UTF-8 utilities |
| `test_atak` | ATAK integration |
| `test_default` | Default configuration helpers |
| `test_http_content_handler` | HTTP handling |
| `test_serial` | Serial communication |
| `test_hop_scaling` | Hop scaling algorithm |
| `test_traffic_management` | Traffic management |
+12 -33
View File
@@ -36,6 +36,7 @@ static MockMeshService *mockMeshService;
// -----------------------------------------------------------------------
// getRegion() tests
// -----------------------------------------------------------------------
extern const RegionInfo *getRegion(meshtastic_Config_LoRaConfig_RegionCode code);
static void test_getRegion_returnsCorrectRegion_US()
{
@@ -103,29 +104,6 @@ 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)
// -----------------------------------------------------------------------
@@ -160,6 +138,7 @@ static const RegionProfile TEST_PROFILE_SPACED = {
/* textThrottle */ 0,
/* positionThrottle */ 0,
/* telemetryThrottle */ 0,
/* overrideSlot */ 0,
};
// A licensed-only profile for testing access control
@@ -172,6 +151,7 @@ static const RegionProfile TEST_PROFILE_LICENSED = {
/* textThrottle */ 5,
/* positionThrottle */ 10,
/* telemetryThrottle */ 10,
/* overrideSlot */ 3,
};
// Turbo-only profile
@@ -184,6 +164,7 @@ static const RegionProfile TEST_PROFILE_TURBO = {
/* textThrottle */ 0,
/* positionThrottle */ 0,
/* telemetryThrottle */ 0,
/* overrideSlot */ 0,
};
// A preset list for the preset-hash override slot test (LONG_FAST + MEDIUM_FAST)
@@ -204,6 +185,7 @@ static const RegionProfile TEST_PROFILE_PRESET_HASH = {
/* textThrottle */ 0,
/* positionThrottle */ 0,
/* telemetryThrottle */ 0,
/* overrideSlot */ OVERRIDE_SLOT_PRESET_HASH,
};
// Standalone test region using US frequencies (26 MHz span → 104 slots at 250 kHz BW)
@@ -218,26 +200,25 @@ static const RegionInfo TEST_REGION_PRESET_HASH = {
false,
&TEST_PROFILE_PRESET_HASH,
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST,
OVERRIDE_SLOT_PRESET_HASH,
"TEST_PRESET_HASH",
};
static const RegionInfo testRegions[] = {
// A wide US-like region with spacing + padding
{meshtastic_Config_LoRaConfig_RegionCode_US, 902.0f, 928.0f, 100, 30, false, false, &TEST_PROFILE_SPACED,
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, 0, "TEST_US_SPACED"},
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, "TEST_US_SPACED"},
// A narrow band simulating tight EU regulation
{meshtastic_Config_LoRaConfig_RegionCode_EU_868, 869.4f, 869.65f, 10, 14, false, false, &TEST_PROFILE_LICENSED,
meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, 3, "TEST_EU_LICENSED"},
meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, "TEST_EU_LICENSED"},
// A wide-LoRa region with turbo-only presets
{meshtastic_Config_LoRaConfig_RegionCode_LORA_24, 2400.0f, 2483.5f, 100, 10, false, true, &TEST_PROFILE_TURBO,
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, 0, "TEST_LORA24_TURBO"},
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, "TEST_LORA24_TURBO"},
// Sentinel — must be last
{meshtastic_Config_LoRaConfig_RegionCode_UNSET, 902.0f, 928.0f, 100, 30, false, false, &TEST_PROFILE_SPACED,
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, 0, "TEST_UNSET"},
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, "TEST_UNSET"},
};
static const RegionInfo *getTestRegion(meshtastic_Config_LoRaConfig_RegionCode code)
@@ -275,7 +256,7 @@ static void test_shadowTable_licensedProfileFlagsCorrect()
const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868);
TEST_ASSERT_TRUE(r->profile->licensedOnly);
TEST_ASSERT_FALSE(r->profile->audioPermitted);
TEST_ASSERT_EQUAL(3, r->overrideSlot);
TEST_ASSERT_EQUAL(3, r->profile->overrideSlot);
}
static void test_shadowTable_presetCountMatchesExpected()
@@ -338,8 +319,8 @@ static void test_shadowTable_unknownCodeFallsToSentinel()
static void test_shadowTable_presetHashProfileHasCorrectOverrideSlot()
{
TEST_ASSERT_EQUAL(OVERRIDE_SLOT_PRESET_HASH, TEST_REGION_PRESET_HASH.overrideSlot);
TEST_ASSERT_EQUAL(-1, TEST_REGION_PRESET_HASH.overrideSlot);
TEST_ASSERT_EQUAL(OVERRIDE_SLOT_PRESET_HASH, TEST_PROFILE_PRESET_HASH.overrideSlot);
TEST_ASSERT_EQUAL(-1, TEST_PROFILE_PRESET_HASH.overrideSlot);
TEST_ASSERT_EQUAL(2, TEST_REGION_PRESET_HASH.getNumPresets());
}
@@ -959,8 +940,6 @@ 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);
-738
View File
@@ -1,738 +0,0 @@
#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 02.
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 07.
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,31 +1,42 @@
#include "../test_helpers.h"
static void assert_encrypted_packet(const std::string &json, const meshtastic_MeshPacket &packet)
// Helper function for all encrypted packet assertions
void assert_encrypted_packet(const std::string &json, meshtastic_MeshPacket packet)
{
// Parse and validate JSON
TEST_ASSERT_TRUE(json.length() > 0);
Json::Value root = parse_json(json);
TEST_ASSERT_TRUE(root.isObject());
JSONValue *root = JSON::Parse(json.c_str());
TEST_ASSERT_NOT_NULL(root);
TEST_ASSERT_TRUE(root->IsObject());
TEST_ASSERT_TRUE(root.isMember("from"));
TEST_ASSERT_EQUAL(packet.from, root["from"].asUInt());
JSONObject jsonObj = root->AsObject();
TEST_ASSERT_TRUE(root.isMember("to"));
TEST_ASSERT_EQUAL(packet.to, root["to"].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("id"));
TEST_ASSERT_EQUAL(packet.id, root["id"].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("bytes"));
TEST_ASSERT_TRUE(root["bytes"].isString());
TEST_ASSERT_TRUE(jsonObj.find("id") != jsonObj.end());
TEST_ASSERT_EQUAL(packet.id, (uint32_t)jsonObj.at("id")->AsNumber());
TEST_ASSERT_TRUE(root.isMember("size"));
TEST_ASSERT_EQUAL(packet.encrypted.size, (int)root["size"].asInt());
// Assert encrypted data fields
TEST_ASSERT_TRUE(jsonObj.find("bytes") != jsonObj.end());
TEST_ASSERT_TRUE(jsonObj.at("bytes")->IsString());
std::string encrypted_hex = root["bytes"].asString();
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();
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";
@@ -37,6 +48,7 @@ void test_encrypted_packet_serialization()
assert_encrypted_packet(json, packet);
}
// Test empty encrypted packet
void test_empty_encrypted_packet()
{
meshtastic_MeshPacket packet =
@@ -13,6 +13,7 @@ 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];
@@ -23,20 +24,28 @@ void test_nodeinfo_serialization()
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
TEST_ASSERT_TRUE(json.length() > 0);
Json::Value root = parse_json(json);
TEST_ASSERT_TRUE(root.isObject());
JSONValue *root = JSON::Parse(json.c_str());
TEST_ASSERT_NOT_NULL(root);
TEST_ASSERT_TRUE(root->IsObject());
TEST_ASSERT_TRUE(root.isMember("type"));
TEST_ASSERT_EQUAL_STRING("nodeinfo", root["type"].asString().c_str());
JSONObject jsonObj = root->AsObject();
TEST_ASSERT_TRUE(root.isMember("payload"));
TEST_ASSERT_TRUE(root["payload"].isObject());
// Check message type
TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end());
TEST_ASSERT_EQUAL_STRING("nodeinfo", jsonObj["type"]->AsString().c_str());
const Json::Value &payload = root["payload"];
// Check payload
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
TEST_ASSERT_TRUE(payload.isMember("shortname"));
TEST_ASSERT_EQUAL_STRING("TEST", payload["shortname"].asString().c_str());
JSONObject payload = jsonObj["payload"]->AsObject();
TEST_ASSERT_TRUE(payload.isMember("longname"));
TEST_ASSERT_EQUAL_STRING("Test User", payload["longname"].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;
}
@@ -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;
position.longitude_i = -1221981000;
position.latitude_i = 374208000; // 37.4208 degrees * 1e7
position.longitude_i = -1221981000; // -122.1981 degrees * 1e7
position.altitude = 123;
position.time = 1609459200;
position.has_altitude = true;
@@ -16,6 +16,7 @@ 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];
@@ -26,23 +27,31 @@ void test_position_serialization()
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
TEST_ASSERT_TRUE(json.length() > 0);
Json::Value root = parse_json(json);
TEST_ASSERT_TRUE(root.isObject());
JSONValue *root = JSON::Parse(json.c_str());
TEST_ASSERT_NOT_NULL(root);
TEST_ASSERT_TRUE(root->IsObject());
TEST_ASSERT_TRUE(root.isMember("type"));
TEST_ASSERT_EQUAL_STRING("position", root["type"].asString().c_str());
JSONObject jsonObj = root->AsObject();
TEST_ASSERT_TRUE(root.isMember("payload"));
TEST_ASSERT_TRUE(root["payload"].isObject());
// Check message type
TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end());
TEST_ASSERT_EQUAL_STRING("position", jsonObj["type"]->AsString().c_str());
const Json::Value &payload = root["payload"];
// Check payload
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
TEST_ASSERT_TRUE(payload.isMember("latitude_i"));
TEST_ASSERT_EQUAL(374208000, payload["latitude_i"].asInt());
JSONObject payload = jsonObj["payload"]->AsObject();
TEST_ASSERT_TRUE(payload.isMember("longitude_i"));
TEST_ASSERT_EQUAL(-1221981000, payload["longitude_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("altitude"));
TEST_ASSERT_EQUAL(123, payload["altitude"].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;
}
@@ -1,62 +1,6 @@
#include "../test_helpers.h"
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;
}
// Helper function to create and encode device metrics
static size_t encode_telemetry_device_metrics(uint8_t *buffer, size_t buffer_size)
{
meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero;
@@ -78,233 +22,507 @@ 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;
fill_all_env_metrics(telemetry);
// 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;
pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size);
pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry);
return stream.bytes_written;
}
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;
}
// Test TELEMETRY_APP port with device metrics
void test_telemetry_device_metrics_serialization()
{
uint8_t buffer[256];
size_t payload_size = encode_telemetry_device_metrics(buffer, sizeof(buffer));
Json::Value root = serialize_and_get_payload(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
TEST_ASSERT_TRUE(root.isMember("type"));
TEST_ASSERT_EQUAL_STRING("telemetry", root["type"].asString().c_str());
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
TEST_ASSERT_TRUE(json.length() > 0);
const Json::Value &payload = root["payload"];
JSONValue *root = JSON::Parse(json.c_str());
TEST_ASSERT_NOT_NULL(root);
TEST_ASSERT_TRUE(root->IsObject());
TEST_ASSERT_TRUE(payload.isMember("battery_level"));
TEST_ASSERT_EQUAL(85, payload["battery_level"].asInt());
JSONObject jsonObj = root->AsObject();
TEST_ASSERT_TRUE(payload.isMember("voltage"));
TEST_ASSERT_FLOAT_WITHIN(0.01f, 3.72f, payload["voltage"].asFloat());
// 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("channel_utilization"));
TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.56f, payload["channel_utilization"].asFloat());
// Check payload
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
TEST_ASSERT_TRUE(payload.isMember("uptime_seconds"));
TEST_ASSERT_EQUAL(12345, payload["uptime_seconds"].asInt());
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 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));
Json::Value root = serialize_and_get_payload(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
const Json::Value &payload = root["payload"];
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
TEST_ASSERT_TRUE(payload.isMember("temperature"));
TEST_ASSERT_FLOAT_WITHIN(0.01f, 23.56f, payload["temperature"].asFloat());
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
TEST_ASSERT_TRUE(json.length() > 0);
TEST_ASSERT_TRUE(payload.isMember("relative_humidity"));
TEST_ASSERT_FLOAT_WITHIN(0.01f, 65.43f, payload["relative_humidity"].asFloat());
JSONValue *root = JSON::Parse(json.c_str());
TEST_ASSERT_NOT_NULL(root);
TEST_ASSERT_TRUE(root->IsObject());
TEST_ASSERT_TRUE(payload.isMember("distance"));
TEST_ASSERT_FLOAT_WITHIN(0.01f, 150.29f, payload["distance"].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 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));
Json::Value root = serialize_and_get_payload(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
const Json::Value &payload = root["payload"];
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
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"));
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 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));
Json::Value root = serialize_and_get_payload(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
const Json::Value &payload = root["payload"];
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
TEST_ASSERT_TRUE(payload.isMember("ir_lux"));
TEST_ASSERT_FLOAT_WITHIN(0.01f, 25.37f, payload["ir_lux"].asFloat());
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
TEST_ASSERT_TRUE(json.length() > 0);
TEST_ASSERT_TRUE(payload.isMember("uv_lux"));
TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.68f, payload["uv_lux"].asFloat());
JSONValue *root = JSON::Parse(json.c_str());
TEST_ASSERT_NOT_NULL(root);
TEST_ASSERT_TRUE(root->IsObject());
TEST_ASSERT_TRUE(payload.isMember("weight"));
TEST_ASSERT_FLOAT_WITHIN(0.01f, 75.56f, payload["weight"].asFloat());
JSONObject jsonObj = root->AsObject();
TEST_ASSERT_TRUE(payload.isMember("rainfall_1h"));
TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.57f, payload["rainfall_1h"].asFloat());
// Check payload exists
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
TEST_ASSERT_TRUE(payload.isMember("rainfall_24h"));
TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.89f, payload["rainfall_24h"].asFloat());
JSONObject payload = jsonObj["payload"]->AsObject();
TEST_ASSERT_TRUE(payload.isMember("soil_moisture"));
TEST_ASSERT_EQUAL(85, payload["soil_moisture"].asInt());
// 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_temperature"));
TEST_ASSERT_FLOAT_WITHIN(0.01f, 18.54f, payload["soil_temperature"].asFloat());
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;
}
// 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.
// 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
void test_telemetry_environment_metrics_complete_coverage()
{
uint8_t buffer[256];
size_t payload_size = encode_telemetry_environment_metrics(buffer, sizeof(buffer));
size_t payload_size = encode_telemetry_environment_metrics_all_fields(buffer, sizeof(buffer));
Json::Value root = serialize_and_get_payload(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
const Json::Value &payload = root["payload"];
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
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());
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
TEST_ASSERT_TRUE(json.length() > 0);
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());
JSONValue *root = JSON::Parse(json.c_str());
TEST_ASSERT_NOT_NULL(root);
TEST_ASSERT_TRUE(root->IsObject());
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());
JSONObject jsonObj = root->AsObject();
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());
// Check payload exists
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
TEST_ASSERT_TRUE(payload.isMember("distance"));
TEST_ASSERT_FLOAT_WITHIN(0.01f, 150.29f, payload["distance"].asFloat());
JSONObject payload = jsonObj["payload"]->AsObject();
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());
// ✅ 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("weight"));
TEST_ASSERT_FLOAT_WITHIN(0.01f, 75.56f, payload["weight"].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("radiation"));
TEST_ASSERT_FLOAT_WITHIN(0.01f, 0.13f, payload["radiation"].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("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());
// 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("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());
// 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 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));
Json::Value root = serialize_and_get_payload(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
const Json::Value &payload = root["payload"];
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
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"));
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;
}
@@ -1,30 +1,48 @@
#include "../test_helpers.h"
#include <memory>
static void verify_text_message_packet_structure(const std::string &json, const char *expected_text)
// Helper function to test common packet fields and structure
void verify_text_message_packet_structure(const std::string &json, const char *expected_text)
{
TEST_ASSERT_TRUE(json.length() > 0);
Json::Value root = parse_json(json);
TEST_ASSERT_TRUE(root.isObject());
// 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());
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());
JSONObject jsonObj = root->AsObject();
TEST_ASSERT_TRUE(root.isMember("type"));
TEST_ASSERT_EQUAL_STRING("text", root["type"].asString().c_str());
// 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("payload"));
TEST_ASSERT_TRUE(root["payload"].isObject());
check_field("from", 0x11223344);
check_field("to", 0x55667788);
check_field("id", 0x9999);
const Json::Value &payload = root["payload"];
TEST_ASSERT_TRUE(payload.isMember("text"));
TEST_ASSERT_EQUAL_STRING(expected_text, payload["text"].asString().c_str());
// 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
}
// Test TEXT_MESSAGE_APP port
void test_text_message_serialization()
{
const char *test_text = "Hello Meshtastic!";
@@ -35,6 +53,7 @@ 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);
@@ -43,9 +62,11 @@ 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()
{
constexpr size_t MAX_MESSAGE_SIZE = 200;
// Test with actual message size limits
constexpr size_t MAX_MESSAGE_SIZE = 200; // Typical LoRa payload limit
std::string long_text(MAX_MESSAGE_SIZE, 'A');
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP,
@@ -55,25 +76,30 @@ 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;
constexpr size_t OVERSIZED_MESSAGE = 250; // Over the limit
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};
const uint8_t invalid_utf8[] = {0xFF, 0xFE, 0xFD, 0x00}; // Invalid UTF-8
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;
waypoint.expire = 1609459200 + 3600; // 1 hour from now
strcpy(waypoint.name, "Test Point");
strcpy(waypoint.description, "Test waypoint description");
@@ -15,6 +15,7 @@ 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];
@@ -25,20 +26,28 @@ void test_waypoint_serialization()
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
TEST_ASSERT_TRUE(json.length() > 0);
Json::Value root = parse_json(json);
TEST_ASSERT_TRUE(root.isObject());
JSONValue *root = JSON::Parse(json.c_str());
TEST_ASSERT_NOT_NULL(root);
TEST_ASSERT_TRUE(root->IsObject());
TEST_ASSERT_TRUE(root.isMember("type"));
TEST_ASSERT_EQUAL_STRING("waypoint", root["type"].asString().c_str());
JSONObject jsonObj = root->AsObject();
TEST_ASSERT_TRUE(root.isMember("payload"));
TEST_ASSERT_TRUE(root["payload"].isObject());
// Check message type
TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end());
TEST_ASSERT_EQUAL_STRING("waypoint", jsonObj["type"]->AsString().c_str());
const Json::Value &payload = root["payload"];
// Check payload
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
TEST_ASSERT_TRUE(payload.isMember("id"));
TEST_ASSERT_EQUAL(12345, payload["id"].asInt());
JSONObject payload = jsonObj["payload"]->AsObject();
TEST_ASSERT_TRUE(payload.isMember("name"));
TEST_ASSERT_EQUAL_STRING("Test Point", payload["name"].asString().c_str());
// 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;
}
+2 -16
View File
@@ -1,9 +1,8 @@
#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>
@@ -11,18 +10,6 @@
#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)
@@ -49,8 +36,7 @@ static meshtastic_MeshPacket create_test_packet(meshtastic_PortNum port, const u
packet.encrypted.size = payload_size;
memcpy(packet.encrypted.bytes, payload, packet.encrypted.size);
}
if (payload && payload_size)
memcpy(packet.decoded.payload.bytes, 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;
+6
View File
@@ -27,6 +27,12 @@
#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.
-18
View File
@@ -33,12 +33,7 @@ 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));
@@ -52,18 +47,6 @@ 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;
@@ -230,7 +213,6 @@ 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);
+6
View File
@@ -5,6 +5,12 @@
#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"
@@ -1,116 +0,0 @@
#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 */
@@ -1,194 +0,0 @@
[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
@@ -1,23 +0,0 @@
#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
}

Some files were not shown because too many files have changed in this diff Show More