Merge branch 'meshtastic:develop' into develop
This commit is contained in:
@@ -11,4 +11,4 @@ runs:
|
|||||||
- name: Install libs needed for native build
|
- name: Install libs needed for native build
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
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 libjsoncpp-dev openssl libssl-dev libulfius-dev liborcania-dev libusb-1.0-0-dev libi2c-dev libuv1-dev libcurl4-gnutls-dev
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ jobs:
|
|||||||
mingw-w64-ucrt-x86_64-libuv
|
mingw-w64-ucrt-x86_64-libuv
|
||||||
mingw-w64-ucrt-x86_64-jsoncpp
|
mingw-w64-ucrt-x86_64-jsoncpp
|
||||||
mingw-w64-ucrt-x86_64-openssl
|
mingw-w64-ucrt-x86_64-openssl
|
||||||
|
mingw-w64-ucrt-x86_64-curl
|
||||||
mingw-w64-ucrt-x86_64-libusb
|
mingw-w64-ucrt-x86_64-libusb
|
||||||
mingw-w64-ucrt-x86_64-cmake
|
mingw-w64-ucrt-x86_64-cmake
|
||||||
mingw-w64-ucrt-x86_64-ninja
|
mingw-w64-ucrt-x86_64-ninja
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ jobs:
|
|||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
MacOS:
|
MacOS:
|
||||||
if: ${{ github.event_name != 'schedule' && github.event.inputs.nightly != 'true' }}
|
if: ${{ !contains(github.ref_name, 'event/') && github.event_name != 'schedule' && github.event.inputs.nightly != 'true' }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -155,7 +155,7 @@ jobs:
|
|||||||
# secrets: inherit
|
# secrets: inherit
|
||||||
|
|
||||||
Windows:
|
Windows:
|
||||||
if: ${{ github.event_name != 'schedule' && github.event.inputs.nightly != 'true' }}
|
if: ${{ !contains(github.ref_name, 'event/') && github.event_name != 'schedule' && github.event.inputs.nightly != 'true' }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -182,7 +182,7 @@ jobs:
|
|||||||
uses: ./.github/workflows/test_native.yml
|
uses: ./.github/workflows/test_native.yml
|
||||||
|
|
||||||
build-wasm:
|
build-wasm:
|
||||||
if: ${{ github.event_name != 'schedule' && github.event.inputs.nightly != 'true' }}
|
if: ${{ !contains(github.ref_name, 'event/') && github.event_name != 'schedule' && github.event.inputs.nightly != 'true' }}
|
||||||
# Build the WebAssembly portduino node ([env:native-wasm]) as part of normal CI,
|
# Build the WebAssembly portduino node ([env:native-wasm]) as part of normal CI,
|
||||||
# like the other platforms. It's a dedicated job (not a row in the `build`
|
# like the other platforms. It's a dedicated job (not a row in the `build`
|
||||||
# matrix) because its artifact is meshnode.{mjs,wasm} - not a flashable
|
# matrix) because its artifact is meshnode.{mjs,wasm} - not a flashable
|
||||||
@@ -191,7 +191,7 @@ jobs:
|
|||||||
uses: ./.github/workflows/build_portduino_wasm.yml
|
uses: ./.github/workflows/build_portduino_wasm.yml
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
if: ${{ github.event_name != 'schedule' && github.event.inputs.nightly != 'true' }}
|
if: ${{ !contains(github.ref_name, 'event/') && github.event_name != 'schedule' && github.event.inputs.nightly != 'true' }}
|
||||||
permissions: # Needed for pushing to GHCR.
|
permissions: # Needed for pushing to GHCR.
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
|||||||
@@ -81,6 +81,14 @@ jobs:
|
|||||||
lcov ${{ env.LCOV_CAPTURE_FLAGS }} --initial --output-file coverage_base.info
|
lcov ${{ env.LCOV_CAPTURE_FLAGS }} --initial --output-file coverage_base.info
|
||||||
sed -i -e "s#${PWD}#.#" coverage_base.info # Make paths relative.
|
sed -i -e "s#${PWD}#.#" coverage_base.info # Make paths relative.
|
||||||
|
|
||||||
|
- name: Config check tests
|
||||||
|
# Drives the same binary against test/fixtures/portduino-config: asserts that
|
||||||
|
# `--check` reports each planted fault, and that a normal run still refuses the
|
||||||
|
# configs it should. Runs before the simulator test because it is seconds long
|
||||||
|
# and a failure here explains a lot of downstream weirdness.
|
||||||
|
timeout-minutes: 5
|
||||||
|
run: ./bin/test-config-check.sh .pio/build/coverage/meshtasticd
|
||||||
|
|
||||||
- name: Integration test
|
- name: Integration test
|
||||||
# Cap the whole step: if the simulator ever fails to exit (e.g. the
|
# Cap the whole step: if the simulator ever fails to exit (e.g. the
|
||||||
# exit_simulator admin path regresses again) the job must fail fast,
|
# exit_simulator admin path regresses again) the job must fail fast,
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
tasks:
|
|
||||||
- init: pip install platformio && pip install --upgrade pip
|
|
||||||
+17
-9
@@ -4,7 +4,7 @@ cli:
|
|||||||
plugins:
|
plugins:
|
||||||
sources:
|
sources:
|
||||||
- id: trunk
|
- id: trunk
|
||||||
ref: v1.10.0
|
ref: v1.10.2
|
||||||
uri: https://github.com/trunk-io/plugins
|
uri: https://github.com/trunk-io/plugins
|
||||||
lint:
|
lint:
|
||||||
# Custom file set + formatter that rewrites Unicode em-dash (U+2014, UTF-8
|
# Custom file set + formatter that rewrites Unicode em-dash (U+2014, UTF-8
|
||||||
@@ -69,19 +69,19 @@ lint:
|
|||||||
enabled:
|
enabled:
|
||||||
- ascii-dash@SYSTEM
|
- ascii-dash@SYSTEM
|
||||||
- too-many-defined@SYSTEM
|
- too-many-defined@SYSTEM
|
||||||
- checkov@3.2.529
|
- checkov@3.3.8
|
||||||
- renovate@43.150.0
|
- renovate@44.2.3
|
||||||
- prettier@3.8.3
|
- prettier@3.9.6
|
||||||
- trufflehog@3.95.3
|
- trufflehog@3.96.0
|
||||||
- yamllint@1.38.0
|
- yamllint@1.38.0
|
||||||
- bandit@1.9.4
|
- bandit@1.9.4
|
||||||
- trivy@0.70.0
|
- trivy@0.72.0
|
||||||
- taplo@0.10.0
|
- taplo@0.10.0
|
||||||
- ruff@0.15.13
|
- ruff@0.16.0
|
||||||
- isort@8.0.1
|
- isort@8.0.1
|
||||||
- markdownlint@0.48.0
|
- markdownlint@0.49.1
|
||||||
- oxipng@10.1.1
|
- oxipng@10.1.1
|
||||||
- svgo@4.0.1
|
- svgo@4.0.2
|
||||||
- actionlint@1.7.12
|
- actionlint@1.7.12
|
||||||
- flake8@7.3.0
|
- flake8@7.3.0
|
||||||
- hadolint@2.14.0
|
- hadolint@2.14.0
|
||||||
@@ -102,6 +102,14 @@ lint:
|
|||||||
- linters: [gitleaks]
|
- linters: [gitleaks]
|
||||||
paths:
|
paths:
|
||||||
- test/fixtures/nodedb/seed_v25_*.jsonl
|
- test/fixtures/nodedb/seed_v25_*.jsonl
|
||||||
|
# Deliberately broken YAML: these are the inputs to bin/test-config-check.sh,
|
||||||
|
# which asserts that `meshtasticd --check` reports each planted fault. Prettier
|
||||||
|
# rejects the duplicate keys and bad indentation that are the whole point of
|
||||||
|
# them, so the directory is exempt. The README there is normal prose and stays
|
||||||
|
# linted.
|
||||||
|
- linters: [ALL]
|
||||||
|
paths:
|
||||||
|
- test/fixtures/portduino-config/**/*.yaml
|
||||||
# checkov/CKV_SECRET_6 flags the commented-out "large4cats" example,
|
# checkov/CKV_SECRET_6 flags the commented-out "large4cats" example,
|
||||||
# which is the public default credential for the meshtastic.org MQTT
|
# which is the public default credential for the meshtastic.org MQTT
|
||||||
# broker rather than a real secret. Ignore the whole file so the
|
# broker rather than a real secret. Ignore the whole file so the
|
||||||
|
|||||||
+2
-2
@@ -16,7 +16,7 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
|
|||||||
curl wget g++ zip git ca-certificates pkg-config \
|
curl wget g++ zip git ca-certificates pkg-config \
|
||||||
python3-pip python3-grpc-tools \
|
python3-pip python3-grpc-tools \
|
||||||
libgpiod-dev libyaml-cpp-dev libjsoncpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
|
libgpiod-dev libyaml-cpp-dev libjsoncpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
|
||||||
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
|
libcurl4-gnutls-dev libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
|
||||||
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \
|
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \
|
||||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
||||||
&& pip install --no-cache-dir -U platformio \
|
&& pip install --no-cache-dir -U platformio \
|
||||||
@@ -55,7 +55,7 @@ USER root
|
|||||||
|
|
||||||
RUN apt-get update && apt-get --no-install-recommends -y install \
|
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 libjsoncpp26 libi2c0 libuv1t64 libusb-1.0-0-dev \
|
||||||
liborcania2.3 libulfius2.7t64 libssl3t64 \
|
libcurl4t64 liborcania2.3 libulfius2.7t64 libssl3t64 \
|
||||||
libx11-6 libinput10 libxkbcommon-x11-0 libsdl2-2.0-0 \
|
libx11-6 libinput10 libxkbcommon-x11-0 libsdl2-2.0-0 \
|
||||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
||||||
&& mkdir -p /var/lib/meshtasticd \
|
&& mkdir -p /var/lib/meshtasticd \
|
||||||
|
|||||||
+2
-2
@@ -17,7 +17,7 @@ ENV PIP_BREAK_SYSTEM_PACKAGES=1
|
|||||||
RUN apk --no-cache add \
|
RUN apk --no-cache add \
|
||||||
bash g++ libstdc++-dev linux-headers zip git ca-certificates libbsd-dev \
|
bash g++ libstdc++-dev linux-headers zip git ca-certificates libbsd-dev \
|
||||||
py3-pip py3-grpcio-tools \
|
py3-pip py3-grpcio-tools \
|
||||||
libgpiod-dev yaml-cpp-dev jsoncpp-dev bluez-dev \
|
libgpiod-dev yaml-cpp-dev jsoncpp-dev bluez-dev curl-dev \
|
||||||
libusb-dev i2c-tools-dev libuv-dev openssl-dev pkgconf argp-standalone \
|
libusb-dev i2c-tools-dev libuv-dev openssl-dev pkgconf argp-standalone \
|
||||||
libx11-dev libinput-dev libxkbcommon-dev sqlite-dev sdl2-dev \
|
libx11-dev libinput-dev libxkbcommon-dev sqlite-dev sdl2-dev \
|
||||||
&& rm -rf /var/cache/apk/* \
|
&& rm -rf /var/cache/apk/* \
|
||||||
@@ -50,7 +50,7 @@ USER root
|
|||||||
|
|
||||||
RUN apk --no-cache add \
|
RUN apk --no-cache add \
|
||||||
shadow libstdc++ libbsd libgpiod yaml-cpp jsoncpp libusb \
|
shadow libstdc++ libbsd libgpiod yaml-cpp jsoncpp libusb \
|
||||||
i2c-tools libuv libx11 libinput libxkbcommon sdl2 \
|
libcurl i2c-tools libuv libx11 libinput libxkbcommon sdl2 \
|
||||||
&& rm -rf /var/cache/apk/* \
|
&& rm -rf /var/cache/apk/* \
|
||||||
&& mkdir -p /var/lib/meshtasticd \
|
&& mkdir -p /var/lib/meshtasticd \
|
||||||
&& mkdir -p /etc/meshtasticd/config.d \
|
&& mkdir -p /etc/meshtasticd/config.d \
|
||||||
|
|||||||
Executable
+391
@@ -0,0 +1,391 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Drive a built meshtasticd against the fixtures in test/fixtures/portduino-config
|
||||||
|
# and assert what it does with each one.
|
||||||
|
#
|
||||||
|
# Why this is a shell test and not a Unity suite: both behaviours under test are
|
||||||
|
# properties of the process, not of a function. `--check` is judged by its exit
|
||||||
|
# status and its printed report, and the "normal run rejects a bad config" path
|
||||||
|
# ends in exit(EXIT_FAILURE) inside portduinoSetup() - neither is reachable from
|
||||||
|
# a native unit test that links a single translation unit.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./bin/test-config-check.sh # auto-detect the binary
|
||||||
|
# ./bin/test-config-check.sh path/to/meshtasticd # explicit binary
|
||||||
|
# MESHTASTICD_BIN=... ./bin/test-config-check.sh
|
||||||
|
#
|
||||||
|
# Exit codes: 0 = GREEN, 1 = RED. The final line is machine-readable:
|
||||||
|
# RESULT: GREEN 42/42 assertions passed
|
||||||
|
# RESULT: RED 2 of 42 assertions failed
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
FIXTURES="$ROOT_DIR/test/fixtures/portduino-config"
|
||||||
|
|
||||||
|
BIN="${1:-${MESHTASTICD_BIN-}}"
|
||||||
|
if [[ -z $BIN ]]; then
|
||||||
|
for candidate in "$ROOT_DIR/.pio/build/native/meshtasticd" "$ROOT_DIR/.pio/build/coverage/meshtasticd"; do
|
||||||
|
[[ -x $candidate ]] && BIN="$candidate" && break
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
if [[ -z $BIN || ! -x $BIN ]]; then
|
||||||
|
echo "RESULT: RED no meshtasticd binary (build one with 'pio run -e native', or pass a path)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Each case runs from a directory of its own choosing, so a relative path (CI passes
|
||||||
|
# .pio/build/coverage/meshtasticd) has to be resolved before the first cd.
|
||||||
|
BIN="$(cd "$(dirname "$BIN")" && pwd)/$(basename "$BIN")"
|
||||||
|
|
||||||
|
# Note: `pio test -e native` writes its Unity test program to this same path, so after a test
|
||||||
|
# run the auto-detected binary is that program rather than the application. That turns out to be
|
||||||
|
# harmless here -- portduino's main() parses argv and runs portduinoSetup(), which prints the
|
||||||
|
# report and exit()s before setup() (Unity's entry point) is ever reached -- so every case below
|
||||||
|
# behaves identically either way. It stops being true only for a case that lets meshtasticd run
|
||||||
|
# past portduinoSetup(), which this suite deliberately never does: a config it accepts boots a
|
||||||
|
# node and blocks.
|
||||||
|
|
||||||
|
# The coverage env links ASan/LSan. Every case here ends in exit(), so LSan would
|
||||||
|
# report the still-reachable config objects and turn a passing case into a non-zero
|
||||||
|
# exit that has nothing to do with what is being asserted.
|
||||||
|
export ASAN_OPTIONS="${ASAN_OPTIONS:-detect_leaks=0}"
|
||||||
|
|
||||||
|
# Keep the virtual filesystem out of the user's home, and run from a directory with
|
||||||
|
# no config.yaml in it so the fixture we name is the only config in play.
|
||||||
|
WORKDIR="$(mktemp -d -t meshcheck.XXXXXX)"
|
||||||
|
trap 'rm -rf "$WORKDIR"' EXIT
|
||||||
|
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
|
||||||
|
# assert <description> <expected-exit> <fixture> <mode> [expected-substring...]
|
||||||
|
# mode: "check" adds --check, "check-yaml" adds --check --output-yaml,
|
||||||
|
# "normal" runs with neither.
|
||||||
|
# fixture: a bare name runs from a scratch directory. A name containing a slash
|
||||||
|
# (configd-conflict/config.yaml) runs from that fixture's own directory,
|
||||||
|
# so a relative ConfigDirectory in it resolves the way it would in situ.
|
||||||
|
assert() {
|
||||||
|
local desc="$1" want_rc="$2" fixture="$3" mode="$4"
|
||||||
|
shift 4
|
||||||
|
|
||||||
|
local cwd="$WORKDIR" config="$FIXTURES/$fixture"
|
||||||
|
if [[ $fixture == */* ]]; then
|
||||||
|
cwd="$FIXTURES/$(dirname "$fixture")"
|
||||||
|
config="$(basename "$fixture")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local args=(--config "$config" -d "$WORKDIR/fs")
|
||||||
|
case $mode in
|
||||||
|
check) args+=(--check) ;;
|
||||||
|
check-yaml) args+=(--check --output-yaml) ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
local out rc
|
||||||
|
# A regression that hangs must fail the test, not stall CI. Every case here is
|
||||||
|
# expected to exit in well under a second.
|
||||||
|
out="$(cd "$cwd" && timeout 60 "$BIN" "${args[@]}" 2>&1)"
|
||||||
|
rc=$?
|
||||||
|
|
||||||
|
local problems=()
|
||||||
|
[[ $rc -ne $want_rc ]] && problems+=("exit $rc, wanted $want_rc")
|
||||||
|
local needle
|
||||||
|
for needle in "$@"; do
|
||||||
|
grep -qF -- "$needle" <<<"$out" || problems+=("missing: $needle")
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ${#problems[@]} -eq 0 ]]; then
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
echo " ok $desc"
|
||||||
|
else
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
echo " FAIL $desc"
|
||||||
|
for problem in "${problems[@]}"; do
|
||||||
|
echo " $problem"
|
||||||
|
done
|
||||||
|
sed 's/^/ | /' <<<"$out"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# A config that meshtasticd fully accepts is asserted clean AND asserted to resolve
|
||||||
|
# to the module we asked for - otherwise a silent fallback to sim would still pass.
|
||||||
|
assert_clean_module() {
|
||||||
|
local fixture="$1" module="$2"
|
||||||
|
assert "$module" 0 "$fixture" check \
|
||||||
|
"Result: 0 errors, 0 warnings" \
|
||||||
|
"Module : $module"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "meshtasticd config-check tests"
|
||||||
|
echo " binary: $BIN"
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "a valid config for every radio module family is clean:"
|
||||||
|
assert_clean_module module-rf95.yaml RF95
|
||||||
|
assert_clean_module module-sx1262.yaml sx1262
|
||||||
|
assert_clean_module module-sx1268.yaml sx1268
|
||||||
|
assert_clean_module module-llcc68.yaml LLCC68
|
||||||
|
assert_clean_module module-sx1280.yaml sx1280
|
||||||
|
assert_clean_module module-lr1110.yaml lr1110
|
||||||
|
assert_clean_module module-lr1120.yaml lr1120
|
||||||
|
assert_clean_module module-lr1121.yaml lr1121
|
||||||
|
assert_clean_module module-sim.yaml sim
|
||||||
|
assert_clean_module module-auto.yaml auto
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "other configs that must not be flagged:"
|
||||||
|
assert "minimal valid config" 0 valid.yaml check \
|
||||||
|
"Result: 0 errors, 0 warnings"
|
||||||
|
assert "empty sections are not a fault" 0 empty-sections.yaml check \
|
||||||
|
"Result: 0 errors, 0 warnings"
|
||||||
|
assert "warnings alone do not fail the run" 0 unknown-key.yaml check \
|
||||||
|
"unknown key 'Lora.Frequency'" \
|
||||||
|
"Result: 0 errors, 1 warning"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "module names are matched exactly:"
|
||||||
|
assert "unknown module names the valid set" 1 module-unknown.yaml check \
|
||||||
|
"Lora.Module 'sx1263' is not a module meshtasticd knows" \
|
||||||
|
"Result: 1 error, 0 warnings"
|
||||||
|
assert "wrong-case module suggests the right spelling" 1 module-wrong-case.yaml check \
|
||||||
|
"did you mean 'LLCC68'?" \
|
||||||
|
"Result: 1 error, 0 warnings"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "LR11xx rfswitch table:"
|
||||||
|
assert "unrecognised switch pin" 1 rfswitch-bad-pin.yaml check \
|
||||||
|
"'DIO9' is not a recognised pin" \
|
||||||
|
"Result: 1 error, 0 warnings"
|
||||||
|
assert "row length must match the pin count" 1 rfswitch-row-length.yaml check \
|
||||||
|
"MODE_STBY has 2 values but 3 pins are declared" \
|
||||||
|
"MODE_RX has 4 values but 3 pins are declared" \
|
||||||
|
"Result: 2 errors, 0 warnings"
|
||||||
|
# Anything not exactly "HIGH" is silently treated as LOW, so lowercase "high" is a
|
||||||
|
# switch that never closes - the failure this check exists to catch.
|
||||||
|
assert "levels must be exactly HIGH or LOW" 1 rfswitch-bad-level.yaml check \
|
||||||
|
"'high' is not HIGH or LOW" \
|
||||||
|
"'On' is not HIGH or LOW" \
|
||||||
|
"Result: 2 errors, 0 warnings"
|
||||||
|
assert "table with no pins list" 1 rfswitch-no-pins.yaml check \
|
||||||
|
"has no 'pins' list, so no switch pins are driven" \
|
||||||
|
"Result: 1 error, 0 warnings"
|
||||||
|
assert "only the first 5 pins are read" 1 rfswitch-too-many-pins.yaml check \
|
||||||
|
"lists 6 pins but only the first 5 are read" \
|
||||||
|
"Result: 1 error, 0 warnings"
|
||||||
|
assert "table must be a mapping" 1 rfswitch-not-a-map.yaml check \
|
||||||
|
"Lora.rfswitch_table must be a mapping" \
|
||||||
|
"Result: 1 error, 0 warnings"
|
||||||
|
assert "unknown MODE_ key" 1 rfswitch-unknown-mode.yaml check \
|
||||||
|
"unknown key 'Lora.rfswitch_table.MODE_TRANSMIT'" \
|
||||||
|
"Result: 1 error, 0 warnings"
|
||||||
|
# One level of over-indentation strands MODE_ rows under Lora: instead of under the
|
||||||
|
# table, where they do nothing. The hint is what makes that findable.
|
||||||
|
assert "MODE_ row stranded outside the table" 0 rfswitch-stranded-modes.yaml check \
|
||||||
|
"unknown key 'Lora.MODE_RX'" \
|
||||||
|
"It is a valid key of Lora.rfswitch_table" \
|
||||||
|
"Result: 0 errors, 1 warning"
|
||||||
|
# A partial table is legal, but the modes left out are driven all-LOW, which for most
|
||||||
|
# modules is the shutdown state - so the report says which ones rather than leaving it
|
||||||
|
# to be discovered on air.
|
||||||
|
assert "omitted modes are called out" 0 rfswitch-partial.yaml check \
|
||||||
|
"omits MODE_GNSS, MODE_TX_HF, MODE_TX_HP, MODE_WIFI" \
|
||||||
|
"default to all pins LOW" \
|
||||||
|
"Result: 0 errors, 0 warnings"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "radio module and switch table must agree:"
|
||||||
|
assert "LR11xx without a table cannot transmit" 0 module-mismatch-lr11xx.yaml check \
|
||||||
|
"Module is lr1121 but no Lora.rfswitch_table is set" \
|
||||||
|
"Result: 0 errors, 1 warning"
|
||||||
|
assert "table on a non-LR11xx radio is ignored" 0 module-mismatch-sx126x.yaml check \
|
||||||
|
"the table is only applied to LR11xx radios" \
|
||||||
|
"Result: 0 errors, 1 warning"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "PA gain table (TX_GAIN_LORA):"
|
||||||
|
# Both shapes are legal and they fail differently. The scalar case is a regression
|
||||||
|
# guard: an earlier version of the checker called this working config a fatal error.
|
||||||
|
assert "a bare scalar is legal" 0 txgain-scalar.yaml check \
|
||||||
|
"Result: 0 errors, 0 warnings"
|
||||||
|
assert "a non-numeric list entry stops meshtasticd" 1 value-type-fatal-list.yaml check \
|
||||||
|
"TX_GAIN_LORA entry 'high' is not a whole number" \
|
||||||
|
"refuses to start" \
|
||||||
|
"Result: 1 error, 0 warnings"
|
||||||
|
assert "entries outside the uint16 range wrap" 1 txgain-out-of-range.yaml check \
|
||||||
|
"entry -5 does not fit the 0-65535 range" \
|
||||||
|
"entry 70000 does not fit the 0-65535 range" \
|
||||||
|
"Result: 2 errors, 0 warnings"
|
||||||
|
assert "points past the 22nd are dropped" 0 txgain-too-many.yaml check \
|
||||||
|
"lists 25 points but only the first 22 are stored" \
|
||||||
|
"Result: 0 errors, 1 warning"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "values of the wrong type:"
|
||||||
|
# The two settings read without a fallback: a bad value throws inside loadConfig() and
|
||||||
|
# meshtasticd will not start. Before this check, --check called these files clean.
|
||||||
|
assert "no-fallback read is fatal" 1 value-type-fatal.yaml check \
|
||||||
|
"Logging.AsciiLogs is not a true/false value" \
|
||||||
|
"refuses to start" \
|
||||||
|
"Result: 1 error, 0 warnings"
|
||||||
|
assert "defaulted reads are silently ignored" 0 value-type-silent.yaml check \
|
||||||
|
"Lora.spiSpeed is not a whole number" \
|
||||||
|
"Lora.DIO2_AS_RF_SWITCH is not a true/false value" \
|
||||||
|
"General.MaxNodes is not a whole number" \
|
||||||
|
"silently replaced by the default" \
|
||||||
|
"Result: 0 errors, 4 warnings"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "out-of-range and unit mistakes:"
|
||||||
|
# The volts/millivolts trap: every other Meshtastic surface says millivolts, so 1800 is
|
||||||
|
# the natural thing to write and it silently asks for 1800V.
|
||||||
|
assert "TCXO voltage written in millivolts" 1 tcxo-millivolts.yaml check \
|
||||||
|
"resolves to 1800000 mV" \
|
||||||
|
"The value is in VOLTS" \
|
||||||
|
"Result: 1 error, 0 warnings"
|
||||||
|
assert "ports outside their usable range" 1 port-out-of-range.yaml check \
|
||||||
|
"General.APIPort 80 is outside 1024-65535" \
|
||||||
|
"Webserver.Port 99999 is not a usable TCP port" \
|
||||||
|
"Result: 1 error, 1 warning"
|
||||||
|
assert "StatusMessage longer than its buffer" 0 statusmessage-long.yaml check \
|
||||||
|
"is truncated to 79 when it is stored" \
|
||||||
|
"Result: 0 errors, 1 warning"
|
||||||
|
# Regression guard for a crash: this used to abort meshtasticd (and --check with it)
|
||||||
|
# via an uncaught filesystem_error from directory_iterator.
|
||||||
|
assert "unreadable ConfigDirectory is reported, not fatal" 1 configdir-missing.yaml check \
|
||||||
|
"is not a directory that can be read" \
|
||||||
|
"Result: 1 error, 0 warnings"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "MAC address sources:"
|
||||||
|
assert "both MACAddress and MACAddressSource" 1 mac-conflict.yaml check \
|
||||||
|
"General.MACAddress and General.MACAddressSource are both set" \
|
||||||
|
"Result: 1 error, 0 warnings"
|
||||||
|
assert "MACAddress that is not 12 hex digits" 1 mac-malformed.yaml check \
|
||||||
|
"is not 12 hex digits" \
|
||||||
|
"Result: 1 error, 0 warnings"
|
||||||
|
assert "MACAddressSource naming no interface" 0 mac-source-missing.yaml check \
|
||||||
|
"has no /sys/class/net/nosuchiface99/address" \
|
||||||
|
"Result: 0 errors, 1 warning"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "pin mappings:"
|
||||||
|
assert "unknown pin sub-key" 1 pin-unknown-subkey.yaml check \
|
||||||
|
"unknown key 'Lora.CS.chipline'" \
|
||||||
|
"A pin mapping accepts only pin, gpiochip and line" \
|
||||||
|
"Result: 1 error, 0 warnings"
|
||||||
|
assert "pin value that resolves to -1" 1 pin-unreadable.yaml check \
|
||||||
|
"Lora.CS is set, but its value could not be read as a pin number" \
|
||||||
|
"Result: 1 error, 0 warnings"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "CH341 USB-SPI adapters:"
|
||||||
|
# The Lora pins of a ch341 device are indexes on the adapter, driven by the usermode
|
||||||
|
# driver: portduinoSetup() skips initGPIOPin() for all of them. Reporting them as
|
||||||
|
# gpiochip lines to confirm with gpioinfo is wrong on Linux and meaningless on the
|
||||||
|
# Windows and macOS hosts where a USB adapter is the only way to attach a radio.
|
||||||
|
assert "adapter pins are not reported as GPIO lines" 0 usb-ch341.yaml check \
|
||||||
|
"CH341 adapter pins (driven over USB, not claimed from a gpiochip)" \
|
||||||
|
"pin indexes on the CH341 itself" \
|
||||||
|
"Module : sx1262" \
|
||||||
|
"Result: 0 errors, 0 warnings"
|
||||||
|
assert "gpiochip mapping on a ch341 device is ignored" 0 ch341-gpiochip.yaml check \
|
||||||
|
"Lora.spidev is ch341" \
|
||||||
|
"Lora.CS.gpiochip, Lora.CS.line, Lora.gpiochip are read but never used" \
|
||||||
|
"Result: 0 errors, 1 warning"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "structural faults:"
|
||||||
|
assert "duplicate key" 1 duplicate-key.yaml check \
|
||||||
|
"duplicate key 'Lora.CS'" \
|
||||||
|
"yaml-cpp keeps the FIRST occurrence" \
|
||||||
|
"Result: 1 error, 0 warnings"
|
||||||
|
assert "section body that is not a mapping" 1 nonmap-section.yaml check \
|
||||||
|
"'Lora' is not a mapping" \
|
||||||
|
"Result: 1 error, 0 warnings"
|
||||||
|
assert "unknown top-level section" 1 unknown-section.yaml check \
|
||||||
|
"unknown top-level section 'Telemetry'" \
|
||||||
|
"Result: 1 error, 0 warnings"
|
||||||
|
assert "key left at the top level" 1 stranded-key.yaml check \
|
||||||
|
"'spidev' is a key of Display or Lora or Touchscreen" \
|
||||||
|
"indent it one level" \
|
||||||
|
"Result: 1 error, 0 warnings"
|
||||||
|
assert "top level is a list" 1 top-level-list.yaml check \
|
||||||
|
"top level is not a mapping" \
|
||||||
|
"Result: 1 error, 0 warnings"
|
||||||
|
assert "empty file" 0 empty-file.yaml check \
|
||||||
|
"is empty" \
|
||||||
|
"Result: 0 errors, 1 warning"
|
||||||
|
# Only the unknown key is asserted: the accompanying "built without HUB75 support"
|
||||||
|
# error depends on whether rgbmatrix was present at build time, so it is not a stable
|
||||||
|
# expectation across builds.
|
||||||
|
assert "unknown HUB75 option" 1 hub75-unknown-key.yaml check \
|
||||||
|
"unknown key 'Display.HUB75.Colours'"
|
||||||
|
# The point of the case: a config that will not parse must still reach the report
|
||||||
|
# with a file and a line, rather than exiting early with just "Unable to use ...".
|
||||||
|
# The asserted line number counts the fixture's comment header - the only assertion
|
||||||
|
# here that moves if you edit those comments.
|
||||||
|
assert "unparseable config still reaches the report" 1 malformed-indent.yaml check \
|
||||||
|
"malformed-indent.yaml" \
|
||||||
|
"could not be parsed" \
|
||||||
|
"error at line 7" \
|
||||||
|
"Result: 1 error, 0 warnings"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "across a config directory:"
|
||||||
|
# Three files each opening a 'Lora:' section. Every key not repeated in the last
|
||||||
|
# one loaded is silently reset to its default - here config.yaml's TCXO voltage.
|
||||||
|
# The warning COUNT is deliberately not asserted: the two config.d files name
|
||||||
|
# different modules, so which one wins - and therefore whether the LR11xx-without-a
|
||||||
|
# switch-table warning fires - depends on the order the filesystem returns them in,
|
||||||
|
# which is the very thing this fixture exists to demonstrate.
|
||||||
|
assert "config.d overrides are reported" 0 configd-conflict/config.yaml check \
|
||||||
|
"config.d/lora-a.yaml" \
|
||||||
|
"config.d/lora-b.yaml" \
|
||||||
|
"not necessarily alphabetical" \
|
||||||
|
"files define a 'Lora:' section" \
|
||||||
|
"The file loaded last wins" \
|
||||||
|
"Result: 0 errors,"
|
||||||
|
# Switch tables are the one place "last wins" is false: the loader only ever writes
|
||||||
|
# HIGH, so the effective table is the OR of every file. Proven with --output-yaml.
|
||||||
|
assert "switch tables across files do not override" 1 rfswitch-sticky/config.yaml check \
|
||||||
|
"These do NOT override each other" \
|
||||||
|
"a HIGH from an earlier file survives a later file that sets LOW" \
|
||||||
|
"Enable exactly one"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "--check takes precedence over --output-yaml:"
|
||||||
|
assert "report wins over the yaml dump" 0 valid.yaml check-yaml \
|
||||||
|
"meshtasticd configuration check" \
|
||||||
|
"Result: 0 errors, 0 warnings"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "normal operation rejects a bad config:"
|
||||||
|
assert "unparseable config is refused" 1 malformed-indent.yaml normal \
|
||||||
|
"Unable to use" \
|
||||||
|
"as config file"
|
||||||
|
assert "non-mapping section is refused" 1 nonmap-section.yaml normal \
|
||||||
|
"Unable to use" \
|
||||||
|
"as config file"
|
||||||
|
assert "unknown module is refused" 1 module-unknown.yaml normal \
|
||||||
|
"Unknown Lora.Module: sx1263"
|
||||||
|
assert "MACAddress conflict is refused" 1 mac-conflict.yaml normal \
|
||||||
|
"Cannot set both MACAddress and MACAddressSource!"
|
||||||
|
# Only meaningful on a build without rgbmatrix. A build that supports HUB75 accepts
|
||||||
|
# the panel and goes on to start a node, which is not something to assert here.
|
||||||
|
# Captured rather than piped: this run exits non-zero by design, and pipefail would
|
||||||
|
# make the pipeline look failed however the grep went.
|
||||||
|
HUB75_PROBE="$("$BIN" --check --config "$FIXTURES/hub75-unknown-key.yaml" -d "$WORKDIR/fs" 2>&1)"
|
||||||
|
if grep -qF "built without HUB75 support" <<<"$HUB75_PROBE"; then
|
||||||
|
assert "unsupported display panel is refused" 1 hub75-unknown-key.yaml normal \
|
||||||
|
"this build does not support HUB75"
|
||||||
|
else
|
||||||
|
echo " skip unsupported display panel is refused (this build has HUB75 support)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
TOTAL=$((PASS + FAIL))
|
||||||
|
if [[ $FAIL -eq 0 ]]; then
|
||||||
|
echo "RESULT: GREEN $PASS/$TOTAL assertions passed"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "RESULT: RED $FAIL of $TOTAL assertions failed"
|
||||||
|
exit 1
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"build": {
|
||||||
|
"arduino": {
|
||||||
|
"ldscript": "nrf52840_s140_v6.ld"
|
||||||
|
},
|
||||||
|
"core": "nRF5",
|
||||||
|
"cpu": "cortex-m4",
|
||||||
|
"extra_flags": "-DARDUINO_NRF52840_ThinkNode_M8 -DNRF52840_XXAA",
|
||||||
|
"f_cpu": "64000000L",
|
||||||
|
"hwids": [
|
||||||
|
["0x239A", "0x4405"],
|
||||||
|
["0x239A", "0x0029"],
|
||||||
|
["0x239A", "0x002A"]
|
||||||
|
],
|
||||||
|
"usb_product": "elecrow_thinknode_m8",
|
||||||
|
"mcu": "nrf52840",
|
||||||
|
"variant": "ELECROW-ThinkNode-M8",
|
||||||
|
"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": "elecrow thinknode m8",
|
||||||
|
"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": "",
|
||||||
|
"vendor": "ELECROW"
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"build": {
|
||||||
|
"arduino": {
|
||||||
|
"ldscript": "esp32s3_out.ld",
|
||||||
|
"memory_type": "qio_opi"
|
||||||
|
},
|
||||||
|
"core": "esp32",
|
||||||
|
"extra_flags": [
|
||||||
|
"-DBOARD_HAS_PSRAM",
|
||||||
|
"-DLILYGO_TBEAM_BPF",
|
||||||
|
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||||
|
"-DARDUINO_USB_MODE=1",
|
||||||
|
"-DARDUINO_RUNNING_CORE=1",
|
||||||
|
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||||
|
],
|
||||||
|
"f_cpu": "240000000L",
|
||||||
|
"f_flash": "80000000L",
|
||||||
|
"flash_mode": "qio",
|
||||||
|
"psram_type": "opi",
|
||||||
|
"hwids": [["0x303A", "0x1001"]],
|
||||||
|
"mcu": "esp32s3",
|
||||||
|
"variant": "t-beam-bpf"
|
||||||
|
},
|
||||||
|
"connectivity": ["wifi", "bluetooth", "lora"],
|
||||||
|
"debug": {
|
||||||
|
"openocd_target": "esp32s3.cfg"
|
||||||
|
},
|
||||||
|
"frameworks": ["arduino"],
|
||||||
|
"name": "LilyGo TBeam-BPF",
|
||||||
|
"upload": {
|
||||||
|
"flash_size": "16MB",
|
||||||
|
"maximum_ram_size": 327680,
|
||||||
|
"maximum_size": 16777216,
|
||||||
|
"require_upload_port": true,
|
||||||
|
"speed": 921600
|
||||||
|
},
|
||||||
|
"url": "http://www.lilygo.cn/",
|
||||||
|
"vendor": "LilyGo"
|
||||||
|
}
|
||||||
+85
-41
@@ -18,6 +18,22 @@ its 4-bit cached role acts as a final fallback when all three identity tiers mis
|
|||||||
Sources of truth: `src/mesh/NodeDB.{h,cpp}`, `src/mesh/WarmNodeStore.h`,
|
Sources of truth: `src/mesh/NodeDB.{h,cpp}`, `src/mesh/WarmNodeStore.h`,
|
||||||
`src/modules/TrafficManagementModule.{h,cpp}`, sizing in `src/mesh/mesh-pb-constants.h`.
|
`src/modules/TrafficManagementModule.{h,cpp}`, sizing in `src/mesh/mesh-pb-constants.h`.
|
||||||
|
|
||||||
|
**Memory classes.** The warm tier (§2) and unified cache (§3) size themselves from
|
||||||
|
`MESHTASTIC_MEM_CLASS` (`src/memory/MemClass.h`), which ranks a build by _usable app heap after
|
||||||
|
platform overheads_ (SoftDevice, WiFi+BLE stacks) rather than by raw RAM or chip family. The hot
|
||||||
|
store (§1) is flash-shaped and the NodeInfo cache (§4) is present-or-absent, so neither is classed:
|
||||||
|
|
||||||
|
| Class | Heap | Parts |
|
||||||
|
| ------ | --------------------- | -------------------------------------------- |
|
||||||
|
| LARGE | PSRAM or host | ESP32-S3 with PSRAM, portduino/native |
|
||||||
|
| MEDIUM | ~250-500 KB, no PSRAM | ESP32-S3/C6/P4 without PSRAM |
|
||||||
|
| SMALL | ~100-250 KB | classic ESP32/S2/C3, nRF52840, RP2040/RP2350 |
|
||||||
|
| TINY | <32 KB | STM32WL |
|
||||||
|
|
||||||
|
An unclassified chip lands in SMALL on purpose: small caches are a recoverable default, an
|
||||||
|
exhausted heap is not. Where a capacity table names a specific part beside these classes, that
|
||||||
|
part is deliberately class-deviant and the reason is given under the table.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. NodeDB hot store (authoritative)
|
## 1. NodeDB hot store (authoritative)
|
||||||
@@ -26,8 +42,6 @@ Sources of truth: `src/mesh/NodeDB.{h,cpp}`, `src/mesh/WarmNodeStore.h`,
|
|||||||
flattened fields (names, role, public key, bitfield flags such as `HAS_XEDDSA_SIGNED`;
|
flattened fields (names, role, public key, bitfield flags such as `HAS_XEDDSA_SIGNED`;
|
||||||
position/telemetry live in satellite stores reached via copy-out accessors, not nested
|
position/telemetry live in satellite stores reached via copy-out accessors, not nested
|
||||||
members). Everything else in this document is a cache or a fallback for it.
|
members). Everything else in this document is a cache or a fallback for it.
|
||||||
- **Capacity:** `MAX_NUM_NODES`, per platform - 250 on native, 120 on nRF52840/generic
|
|
||||||
ESP32, 10 on STM32WL (see `mesh-pb-constants.h`).
|
|
||||||
- **Eviction:** oldest non-protected node when full (`getOrCreateMeshNode`). On eviction
|
- **Eviction:** oldest non-protected node when full (`getOrCreateMeshNode`). On eviction
|
||||||
the node's essentials are **absorbed into the warm tier** (see §2); on re-admission the
|
the node's essentials are **absorbed into the warm tier** (see §2); on re-admission the
|
||||||
warm record is rehydrated back (`take()`), including the signer bit.
|
warm record is rehydrated back (`take()`), including the signer bit.
|
||||||
@@ -45,6 +59,17 @@ Sources of truth: `src/mesh/NodeDB.{h,cpp}`, `src/mesh/WarmNodeStore.h`,
|
|||||||
warm-evicted signer be impersonated with unsigned frames.
|
warm-evicted signer be impersonated with unsigned frames.
|
||||||
- `getNodeRole(n)` - hot store, then the role cached in the warm tier, else `CLIENT`.
|
- `getNodeRole(n)` - hot store, then the role cached in the warm tier, else `CLIENT`.
|
||||||
|
|
||||||
|
**Capacity** - `MAX_NUM_NODES` (`mesh-pb-constants.h`):
|
||||||
|
|
||||||
|
| ESP32-S3 | Native | nRF52840, generic ESP32 | STM32WL |
|
||||||
|
| --------------- | ------ | ----------------------- | ------- |
|
||||||
|
| 250 / 200 / 100 | 250 | 120 | 10 |
|
||||||
|
|
||||||
|
This one is flash-shaped rather than heap-shaped, so it is unclassed: `nodes.proto` has to fit the
|
||||||
|
filesystem. ESP32-S3 is the only runtime tier, picked at boot from the flash chip (>=15 MB />=7 MB
|
||||||
|
/ smaller); the 120 covers nRF52840 plus generic ESP32 including C3, and is what keeps `nodes.proto`
|
||||||
|
inside the stock 28 KB LittleFS.
|
||||||
|
|
||||||
## 2. Warm tier - `WarmNodeStore` (NodeDB-owned)
|
## 2. Warm tier - `WarmNodeStore` (NodeDB-owned)
|
||||||
|
|
||||||
- **What:** the "long-tail" second tier. When a node ages out of the hot store, a minimal
|
- **What:** the "long-tail" second tier. When a node ages out of the hot store, a minimal
|
||||||
@@ -53,7 +78,6 @@ Sources of truth: `src/mesh/NodeDB.{h,cpp}`, `src/mesh/WarmNodeStore.h`,
|
|||||||
- **Entry:** exactly 40 bytes - `num(4) | last_heard(4) | public_key(32)`. The low 7 bits
|
- **Entry:** exactly 40 bytes - `num(4) | last_heard(4) | public_key(32)`. The low 7 bits
|
||||||
of `last_heard` are omitted, and replaced with metadata (role: 4 bits, protected
|
of `last_heard` are omitted, and replaced with metadata (role: 4 bits, protected
|
||||||
category: 2, signer bit: 1), leaving ~128 s recency resolution - plenty for LRU ranking.
|
category: 2, signer bit: 1), leaving ~128 s recency resolution - plenty for LRU ranking.
|
||||||
- **Capacity:** `WARM_NODE_COUNT` (100 on constrained parts; platform-tiered).
|
|
||||||
- **Eviction:** LRU by `last_heard`, with keyed entries outranking keyless; keyless
|
- **Eviction:** LRU by `last_heard`, with keyed entries outranking keyless; keyless
|
||||||
candidates never displace keyed entries.
|
candidates never displace keyed entries.
|
||||||
- **Persistence:** nRF52840 uses a 12 KB raw-flash record-ring below LittleFS
|
- **Persistence:** nRF52840 uses a 12 KB raw-flash record-ring below LittleFS
|
||||||
@@ -61,6 +85,18 @@ Sources of truth: `src/mesh/NodeDB.{h,cpp}`, `src/mesh/WarmNodeStore.h`,
|
|||||||
- **Membership invariant:** a node lives in the hot **XOR** warm tier. `take()` removes
|
- **Membership invariant:** a node lives in the hot **XOR** warm tier. `take()` removes
|
||||||
the warm record when the node is re-admitted hot, restoring role/protected/signer bits.
|
the warm record when the node is re-admitted hot, restoring role/protected/signer bits.
|
||||||
|
|
||||||
|
**Capacity** - `WARM_NODE_COUNT` (`mesh-pb-constants.h`):
|
||||||
|
|
||||||
|
| LARGE | MEDIUM | RP2040 / RP2350 | nRF52840 | SMALL | TINY |
|
||||||
|
| ----- | ------ | --------------- | -------- | ----- | ---- |
|
||||||
|
| 2000 | 150 | 150 | 100 | 100 | 0 |
|
||||||
|
|
||||||
|
TINY's 0 disables the tier outright. At 40 B/entry, LARGE costs ~80 KB and lives in PSRAM, MEDIUM
|
||||||
|
~6 KB of heap. Both named parts are class-deviant on purpose: RP2040/RP2350 is bounded so the
|
||||||
|
`warm.dat` write fits the 8 s watchdog (#10746) rather than by RAM, and nRF52840 dropped from 200 to
|
||||||
|
100 because its RAM cache is calloc'd from the ~115 KB heap arena shared with SoftDevice, which
|
||||||
|
2.8.0 field reports showed at 99% use.
|
||||||
|
|
||||||
## 3. TMM unified cache (base, traffic state)
|
## 3. TMM unified cache (base, traffic state)
|
||||||
|
|
||||||
- **What:** TMM's own flat array of packed 10-byte `UnifiedCacheEntry` records - the
|
- **What:** TMM's own flat array of packed 10-byte `UnifiedCacheEntry` records - the
|
||||||
@@ -76,15 +112,22 @@ Sources of truth: `src/mesh/NodeDB.{h,cpp}`, `src/mesh/WarmNodeStore.h`,
|
|||||||
`node(4) | pos_fingerprint(1) | rate_count(1) | unknown_count(1) | pos_time(1) | rate_unknown_time(1) | next_hop(1)`
|
`node(4) | pos_fingerprint(1) | rate_count(1) | unknown_count(1) | pos_time(1) | rate_unknown_time(1) | next_hop(1)`
|
||||||
= 10 bytes, all platforms. Timestamps are free-running modular ticks (uint8 / nibbles)
|
= 10 bytes, all platforms. Timestamps are free-running modular ticks (uint8 / nibbles)
|
||||||
with presence carried by non-zero sentinels - no epochs, no absolute time.
|
with presence carried by non-zero sentinels - no epochs, no absolute time.
|
||||||
- **Capacity:** `TRAFFIC_MANAGEMENT_CACHE_SIZE`, per memory class: 2048 (PSRAM S3 /
|
|
||||||
native), 500 (medium), 400 (small), 250 (nRF52840 - deliberately class-deviant for heap
|
|
||||||
headroom), 0 when `HAS_TRAFFIC_MANAGEMENT=0`. Variant-overridable.
|
|
||||||
- **Eviction:** linear scan; insertion on a full cache evicts the stalest entry,
|
- **Eviction:** linear scan; insertion on a full cache evicts the stalest entry,
|
||||||
preferring to keep entries with a `next_hop` hint **or** a cached special (non-`CLIENT`)
|
preferring to keep entries with a `next_hop` hint **or** a cached special (non-`CLIENT`)
|
||||||
role - the long-tail state this cache exists to retain (`findOrCreateEntry`'s `preferred`
|
role - the long-tail state this cache exists to retain (`findOrCreateEntry`'s `preferred`
|
||||||
test covers both, not just `next_hop`).
|
test covers both, not just `next_hop`).
|
||||||
- **Persistence:** none - RAM/PSRAM only, rebuilt from traffic.
|
- **Persistence:** none - RAM/PSRAM only, rebuilt from traffic.
|
||||||
|
|
||||||
|
**Capacity** - `TRAFFIC_MANAGEMENT_CACHE_SIZE` (`mesh-pb-constants.h`), variant-overridable:
|
||||||
|
|
||||||
|
| LARGE | MEDIUM | SMALL | nRF52840 | `HAS_TRAFFIC_MANAGEMENT=0` |
|
||||||
|
| ----- | ------ | ----- | -------- | -------------------------- |
|
||||||
|
| 2048 | 500 | 400 | 250 | 0 |
|
||||||
|
|
||||||
|
At 10 B/entry that is ~5 KB on MEDIUM and ~2.5 KB on nRF52840, which is class-deviant for the same
|
||||||
|
heap reason as the warm tier (its class would give 400); 250 entries still tracks over 2x the
|
||||||
|
120-node hot store, and LRU victim recycling absorbs busier meshes.
|
||||||
|
|
||||||
## 4. TMM NodeInfo payload cache (extended, the ephemeral third tier)
|
## 4. TMM NodeInfo payload cache (extended, the ephemeral third tier)
|
||||||
|
|
||||||
- **What:** a flat array of `NodeInfoPayloadEntry` (PSRAM-backed on hardware; see
|
- **What:** a flat array of `NodeInfoPayloadEntry` (PSRAM-backed on hardware; see
|
||||||
@@ -101,11 +144,20 @@ Sources of truth: `src/mesh/NodeDB.{h,cpp}`, `src/mesh/WarmNodeStore.h`,
|
|||||||
`keySignerProven`, `hasObserved`, `hasFullUser`, `isMember`. (The direct-response throttle
|
`keySignerProven`, `hasObserved`, `hasFullUser`, `isMember`. (The direct-response throttle
|
||||||
no longer keeps per-entry state here - it is a pair of separate RAM tables; see the module
|
no longer keeps per-entry state here - it is a pair of separate RAM tables; see the module
|
||||||
doc.)
|
doc.)
|
||||||
- **Capacity:** `kNodeInfoCacheEntries = 2000`, linear scan (NodeInfo traffic is
|
|
||||||
low-rate).
|
|
||||||
- **Persistence:** none - this tier is deliberately ephemeral; it reconstructs from NodeDB
|
- **Persistence:** none - this tier is deliberately ephemeral; it reconstructs from NodeDB
|
||||||
seeding plus observed traffic after every boot.
|
seeding plus observed traffic after every boot.
|
||||||
|
|
||||||
|
**Capacity** - `kNodeInfoCacheEntries` (`TrafficManagementModule.h`), gated by
|
||||||
|
`TMM_HAS_NODEINFO_CACHE`:
|
||||||
|
|
||||||
|
| ESP32 + PSRAM | Native unit-test builds | Everything else |
|
||||||
|
| ------------- | ----------------------- | --------------- |
|
||||||
|
| 2000 | 2000 | not compiled |
|
||||||
|
|
||||||
|
Not class-tiered: the array is either compiled or it isn't. ESP32+PSRAM is the production home (in
|
||||||
|
PSRAM); native test builds put the same 2000 entries on the plain heap so the trust and retention
|
||||||
|
paths run in CI. Linear scan in every build - NodeInfo traffic is low-rate.
|
||||||
|
|
||||||
### Trust & provenance model
|
### Trust & provenance model
|
||||||
|
|
||||||
- **Key pin, three layers deep:** an incoming NodeInfo key is checked against
|
- **Key pin, three layers deep:** an incoming NodeInfo key is checked against
|
||||||
@@ -172,22 +224,14 @@ rehydration.
|
|||||||
|
|
||||||
### Tick clocks and wrap safety
|
### Tick clocks and wrap safety
|
||||||
|
|
||||||
All TMM timestamps are free-running modular ticks (uint8 or nibble) from `clockMs()`; modular
|
This cache's `obsTick` recency stamp, like the unified cache's pos/rate/unknown stamps, is a
|
||||||
subtraction is correct only while the true age stays below the counter period, so every clock
|
free-running modular tick rather than an absolute time, and depends on the maintenance sweep to
|
||||||
needs something to clear expired state before it aliases.
|
clear expired state before it aliases. The per-clock periods, windows, and what keeps each honest
|
||||||
|
are documented with the module in
|
||||||
| Clock | Tick / period | Window | Kept honest by |
|
[traffic_management_module.md](traffic_management_module.md#tick-clocks-and-wrap-safety). The sharp
|
||||||
| ------------------ | -------------- | --------------- | -------------------------------------------------- |
|
case for this tier is `obsTick`: the sweep clearing `hasObserved` is the _sole_ guarantee the 6 h
|
||||||
| pos | 6 min / 25.6 h | <=255 ticks | 60 s sweep (margin as low as 1 tick at the clamp) |
|
serve gate never reads an aliased stamp, which is why it is a compile-time invariant guarded by
|
||||||
| rate | 5 min / 80 min | <=15 ticks | sweep + read-time window reset (`isRateLimited()`) |
|
`TMM_HAS_NODEINFO_CACHE` alone.
|
||||||
| unknown | 1 min / 16 min | 12 ticks | sweep + read-time window reset |
|
|
||||||
| NodeInfo `obsTick` | 3 min / 12.8 h | 120 ticks (6 h) | sweep only |
|
|
||||||
|
|
||||||
`obsTick` is the sharp case: `maintainNodeInfoCacheLocked()` clearing `hasObserved` is the
|
|
||||||
_sole_ guarantee the 6 h serve gate never reads an aliased stamp. That makes the sweep a
|
|
||||||
compile-time invariant - guarded by `TMM_HAS_NODEINFO_CACHE` **alone** (never
|
|
||||||
`TRAFFIC_MANAGEMENT_CACHE_SIZE`, which a variant may zero independently), mirroring `purgeAll()`:
|
|
||||||
a build that has the cache always has its sweep.
|
|
||||||
|
|
||||||
The warm tier is different by design: `WarmNodeStore.last_heard` is an **absolute** unix-seconds
|
The warm tier is different by design: `WarmNodeStore.last_heard` is an **absolute** unix-seconds
|
||||||
timestamp (128 s quantised), so it cannot wrap until 2106 and needs no sweep - the TMM caches
|
timestamp (128 s quantised), so it cannot wrap until 2106 and needs no sweep - the TMM caches
|
||||||
@@ -207,23 +251,23 @@ behaviour - is documented with the module in
|
|||||||
Side-by-side view of what each store actually holds ("-" = not held). Details and
|
Side-by-side view of what each store actually holds ("-" = not held). Details and
|
||||||
rationale live in the per-store sections above.
|
rationale live in the per-store sections above.
|
||||||
|
|
||||||
| Property | 1. Hot store (`NodeInfoLite`) | 2. Warm tier (`WarmNodeEntry`) | 3. NodeInfo cache (`NodeInfoPayloadEntry`) | 4. Unified cache (`UnifiedCacheEntry`) |
|
| Property | 1. Hot store (`NodeInfoLite`) | 2. Warm tier (`WarmNodeEntry`) | 3. NodeInfo cache (`NodeInfoPayloadEntry`) | 4. Unified cache (`UnifiedCacheEntry`) |
|
||||||
| ------------------------- | -------------------------------- | ---------------------------------------------- | ------------------------------------------------------------- | ---------------------------------------------------- |
|
| ------------------------- | -------------------------------- | ---------------------------------------------- | ------------------------------------------------------------- | -------------------------------------------------- |
|
||||||
| Node number | yes | yes | yes (0 = free slot) | yes (0 = free slot) |
|
| Node number | yes | yes | yes (0 = free slot) | yes (0 = free slot) |
|
||||||
| Names + user id | yes (flattened fields) | - | yes (full `User`, when `hasFullUser`) | - |
|
| Names + user id | yes (flattened fields) | - | yes (full `User`, when `hasFullUser`) | - |
|
||||||
| Public key (32 B) | yes (authoritative) | yes (keyed entries) | yes (TOFU or proven; pinned against tiers 1-2) | - |
|
| Public key (32 B) | yes (authoritative) | yes (keyed entries) | yes (TOFU or proven; pinned against tiers 1-2) | - |
|
||||||
| Signer provenance | `HAS_XEDDSA_SIGNED` bitfield bit | 1 signer bit (shared with `last_heard`) | `keySignerProven` (monotonic per key) | - |
|
| Signer provenance | `HAS_XEDDSA_SIGNED` bitfield bit | 1 signer bit (shared with `last_heard`) | `keySignerProven` (monotonic per key) | - |
|
||||||
| Device role | `role` field | 4-bit role (metadata steal) | inside the cached `User` | 4-bit role in count-byte top bits (final fallback) |
|
| Device role | `role` field | 4-bit role (metadata steal) | inside the cached `User` | 4-bit role in count-byte top bits (final fallback) |
|
||||||
| Recency | `last_heard` (unix secs) | `last_heard` (unix secs, 128 s quantised) | `obsTick` (3 min modular tick) + `hasObserved` | pos/rate/unknown modular ticks |
|
| Recency | `last_heard` (unix secs) | `last_heard` (unix secs, 128 s quantised) | `obsTick` (3 min modular tick) + `hasObserved` | pos/rate/unknown modular ticks |
|
||||||
| Position / telemetry | via satellite copy-out accessors | - | - | 8-bit position _fingerprint_ only (dedup) |
|
| Position / telemetry | via satellite copy-out accessors | - | - | 8-bit position _fingerprint_ only (dedup) |
|
||||||
| Protected / favorite | bitfield flags | 2-bit protected category | - (`isMember` keep-alive instead) | - |
|
| Protected / favorite | bitfield flags | 2-bit protected category | - (`isMember` keep-alive instead) | - |
|
||||||
| Routing hint (`next_hop`) | yes (persisted field) | - | - | ACK-confirmed relay byte (preloaded from tier 1) |
|
| Routing hint (`next_hop`) | yes (persisted field) | - | - | ACK-confirmed relay byte (preloaded from tier 1) |
|
||||||
| Direct-reply metadata | - | - | `sourceChannel`, `decodedBitfield` (+ `hasDecodedBitfield`) | - |
|
| Direct-reply metadata | - | - | `sourceChannel`, `decodedBitfield` (+ `hasDecodedBitfield`) | - |
|
||||||
| Traffic-shaping counters | - | - | - | rate + unknown counts, pos fingerprint |
|
| Traffic-shaping counters | - | - | - | rate + unknown counts, pos fingerprint |
|
||||||
| Entry size | largest (full struct) | 40 B exact | ~`sizeof(User)`+8, platform-padded (no size assert by design) | 10 B exact |
|
| Entry size | largest (full struct) | 40 B exact | ~`sizeof(User)`+8, platform-padded (no size assert by design) | 10 B exact |
|
||||||
| Capacity | `MAX_NUM_NODES` (250/120/10) | `WARM_NODE_COUNT` (~100) | `kNodeInfoCacheEntries` (2000) | `TRAFFIC_MANAGEMENT_CACHE_SIZE` (2048/500/400/250/0) |
|
| Capacity | `MAX_NUM_NODES` (10-250) | `WARM_NODE_COUNT` (0-2000) | `kNodeInfoCacheEntries` (2000) | `TRAFFIC_MANAGEMENT_CACHE_SIZE` (0-2048) |
|
||||||
| Persistence | node DB file | raw-flash ring (nRF52840) or `/prefs/warm.dat` | none (rebuilt from seed + traffic) | none |
|
| Persistence | node DB file | raw-flash ring (nRF52840) or `/prefs/warm.dat` | none (rebuilt from seed + traffic) | none |
|
||||||
| Storage | RAM | RAM + flash | PSRAM on hardware; plain heap in native tests | PSRAM when available, else heap |
|
| Storage | RAM | RAM + flash | PSRAM on hardware; plain heap in native tests | PSRAM when available, else heap |
|
||||||
|
|
||||||
## How a lookup falls through the tiers
|
## How a lookup falls through the tiers
|
||||||
|
|
||||||
|
|||||||
@@ -177,6 +177,33 @@ carries throttle state.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Tick clocks and wrap safety
|
||||||
|
|
||||||
|
Every per-node timestamp in TMM's caches is a free-running modular tick (uint8 or nibble) taken
|
||||||
|
from `clockMs()` - never an absolute time. That is what keeps `UnifiedCacheEntry` at 10 bytes
|
||||||
|
across up to 2048 entries. The cost is that modular subtraction is only correct while the true age
|
||||||
|
stays below the counter's period, so every clock needs something to clear expired state before it
|
||||||
|
aliases. (The direct-serve throttle above is the deliberate exception: full `uint32` milliseconds
|
||||||
|
compared by wrap-safe subtraction, hence no tick and no sweep.)
|
||||||
|
|
||||||
|
| Clock | Tick / period | Window | Kept honest by |
|
||||||
|
| ------------------ | -------------- | --------------- | -------------------------------------------------- |
|
||||||
|
| pos | 6 min / 25.6 h | <=255 ticks | 60 s sweep (margin as low as 1 tick at the clamp) |
|
||||||
|
| rate | 5 min / 80 min | <=15 ticks | sweep + read-time window reset (`isRateLimited()`) |
|
||||||
|
| unknown | 1 min / 16 min | 12 ticks | sweep + read-time window reset |
|
||||||
|
| NodeInfo `obsTick` | 3 min / 12.8 h | 120 ticks (6 h) | sweep only |
|
||||||
|
|
||||||
|
`obsTick` is the sharp case: `maintainNodeInfoCacheLocked()` clearing `hasObserved` is the
|
||||||
|
_sole_ guarantee the 6 h serve gate never reads an aliased stamp. That makes the sweep a
|
||||||
|
compile-time invariant - guarded by `TMM_HAS_NODEINFO_CACHE` **alone** (never
|
||||||
|
`TRAFFIC_MANAGEMENT_CACHE_SIZE`, which a variant may zero independently), mirroring `purgeAll()`:
|
||||||
|
a build that has the cache always has its sweep.
|
||||||
|
|
||||||
|
The stores these clocks stamp, and the warm tier's contrasting absolute timestamps, are described
|
||||||
|
in [node_info_stores.md](node_info_stores.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
All tunables live under `moduleConfig.traffic_management`; the whole module is gated by the
|
All tunables live under `moduleConfig.traffic_management`; the whole module is gated by the
|
||||||
|
|||||||
+1
-1
@@ -128,7 +128,7 @@ lib_deps =
|
|||||||
[device-ui_base]
|
[device-ui_base]
|
||||||
lib_deps =
|
lib_deps =
|
||||||
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
|
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
|
||||||
https://github.com/meshtastic/device-ui/archive/ef573c368767625ffbe8c32cf921ea7366f2dd53.zip
|
https://github.com/meshtastic/device-ui/archive/bcb327f058469282408eed93e27249d6447de9c0.zip
|
||||||
|
|
||||||
; Common libs for environmental measurements in telemetry module
|
; Common libs for environmental measurements in telemetry module
|
||||||
[environmental_base]
|
[environmental_base]
|
||||||
|
|||||||
+1
-1
Submodule protobufs updated: bfd718fa1d...cd290ba246
@@ -215,7 +215,12 @@ const StoredMessage *MessageStore::tryAddFromPacket(const meshtastic_MeshPacket
|
|||||||
sm.channelIndex = packet.channel;
|
sm.channelIndex = packet.channel;
|
||||||
|
|
||||||
const char *payload = reinterpret_cast<const char *>(packet.decoded.payload.bytes);
|
const char *payload = reinterpret_cast<const char *>(packet.decoded.payload.bytes);
|
||||||
size_t len = strnlen(payload, MAX_MESSAGE_SIZE - 1);
|
// payload.bytes is not NUL-terminated, so bound by the received size too: a shorter message
|
||||||
|
// stored after a longer one would otherwise pick up the previous occupant's trailing bytes.
|
||||||
|
size_t avail = packet.decoded.payload.size;
|
||||||
|
if (avail > MAX_MESSAGE_SIZE - 1)
|
||||||
|
avail = MAX_MESSAGE_SIZE - 1;
|
||||||
|
size_t len = strnlen(payload, avail);
|
||||||
sm.textOffset = storeTextInPool(payload, len);
|
sm.textOffset = storeTextInPool(payload, len);
|
||||||
sm.textLength = len;
|
sm.textLength = len;
|
||||||
|
|
||||||
|
|||||||
+17
-1
@@ -566,7 +566,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
// technically speaking this should work for all(?) NRF52 boards
|
// technically speaking this should work for all(?) NRF52 boards
|
||||||
// but needs testing across multiple devices. NRF52 USB would not even work if
|
// but needs testing across multiple devices. NRF52 USB would not even work if
|
||||||
// VBUS was not properly connected and detected by the CPU
|
// VBUS was not properly connected and detected by the CPU
|
||||||
#elif defined(MUZI_BASE) || defined(PROMICRO_DIY_TCXO)
|
#elif defined(MUZI_BASE) || defined(PROMICRO_DIY_TCXO) || defined(ELECROW_ThinkNode_M8)
|
||||||
return powerHAL_isVBUSConnected();
|
return powerHAL_isVBUSConnected();
|
||||||
#endif
|
#endif
|
||||||
return getBattVoltage() > chargingVolt;
|
return getBattVoltage() > chargingVolt;
|
||||||
@@ -1401,6 +1401,22 @@ bool Power::axpChipInit()
|
|||||||
PMU->disablePowerOutput(XPOWERS_DLDO1); // Invalid power channel, it does not exist
|
PMU->disablePowerOutput(XPOWERS_DLDO1); // Invalid power channel, it does not exist
|
||||||
PMU->disablePowerOutput(XPOWERS_DLDO2); // Invalid power channel, it does not exist
|
PMU->disablePowerOutput(XPOWERS_DLDO2); // Invalid power channel, it does not exist
|
||||||
PMU->disablePowerOutput(XPOWERS_VBACKUP);
|
PMU->disablePowerOutput(XPOWERS_VBACKUP);
|
||||||
|
} else if (HW_VENDOR == meshtastic_HardwareModel_TBEAM_BPF) {
|
||||||
|
// T-Beam BPF rail map (per schematic LilyGo_TBeam_BPF r2025-05-08):
|
||||||
|
// DCDC1 -> ESP32 + OLED 3V3 (always on, protected)
|
||||||
|
// ALDO2 -> MicroSD 3V3 (OFF at reset, must enable)
|
||||||
|
// ALDO4 -> L76K GNSS 3V3 (OFF at reset, must enable)
|
||||||
|
// ALDO1/3, BLDO1/2, DLDO1 -> user headers / unused at boot, leave at reset defaults.
|
||||||
|
// LoRa power is outside the PMU (external P-MOSFET switched by RF95_POWER_EN / IO16).
|
||||||
|
PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 3300);
|
||||||
|
PMU->enablePowerOutput(XPOWERS_ALDO4);
|
||||||
|
|
||||||
|
PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300);
|
||||||
|
PMU->enablePowerOutput(XPOWERS_ALDO2);
|
||||||
|
|
||||||
|
// Make sure nothing's driving into an unused rail
|
||||||
|
PMU->disablePowerOutput(XPOWERS_DCDC5);
|
||||||
|
PMU->disablePowerOutput(XPOWERS_DLDO1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// disable all axp chip interrupt
|
// disable all axp chip interrupt
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// See UptimeClock.h for the full contract.
|
||||||
|
#include "UptimeClock.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
uint32_t Time::getMillis()
|
||||||
|
{
|
||||||
|
#ifdef PIO_UNIT_TESTING
|
||||||
|
if (Time::useTestClock)
|
||||||
|
return Time::testNowMs;
|
||||||
|
#endif
|
||||||
|
return millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t Time::getMillis64()
|
||||||
|
{
|
||||||
|
static uint32_t lastLow = 0; // last 32-bit sample
|
||||||
|
static uint32_t highWord = 0; // number of observed wraps
|
||||||
|
|
||||||
|
uint32_t now = Time::getMillis();
|
||||||
|
#ifdef PIO_UNIT_TESTING
|
||||||
|
// A test swapping clock sources (real <-> injected) can make `now` jump backward for
|
||||||
|
// reasons other than a genuine wrap - rebase rather than miscount it as one.
|
||||||
|
if (Time::clockSourceChanged) {
|
||||||
|
lastLow = now;
|
||||||
|
highWord = 0;
|
||||||
|
Time::clockSourceChanged = false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if (now < lastLow)
|
||||||
|
highWord++; // low word wrapped since last call
|
||||||
|
lastLow = now;
|
||||||
|
return (static_cast<uint64_t>(highWord) << 32) | now;
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
// Monotonic uptime clock, injectable so tests can drive a virtual timebase instead of sleeping.
|
||||||
|
// Uptime only; see gps/RTC.h for wall-clock. Not named Time.h: -Isrc would shadow C's <time.h>.
|
||||||
|
namespace Time
|
||||||
|
{
|
||||||
|
#ifdef PIO_UNIT_TESTING
|
||||||
|
// Test-only virtual clock; OFF by default so suites relying on real time are unaffected.
|
||||||
|
inline uint32_t testNowMs = 0;
|
||||||
|
inline bool useTestClock = false;
|
||||||
|
inline bool clockSourceChanged = true; // forces getMillis64() to rebase its wrap accumulator
|
||||||
|
|
||||||
|
inline void setTestMillis(uint32_t ms)
|
||||||
|
{
|
||||||
|
testNowMs = ms;
|
||||||
|
useTestClock = true;
|
||||||
|
clockSourceChanged = true;
|
||||||
|
}
|
||||||
|
inline void advanceTestMillis(uint32_t deltaMs)
|
||||||
|
{
|
||||||
|
// Advancing from 0 after getMillis64() sampled the real clock steps backward, which would
|
||||||
|
// otherwise be miscounted as a wrap.
|
||||||
|
if (!useTestClock)
|
||||||
|
clockSourceChanged = true;
|
||||||
|
testNowMs += deltaMs;
|
||||||
|
useTestClock = true;
|
||||||
|
}
|
||||||
|
// Restore real-clock behaviour (call in test tearDown if a suite mixes real and fake time).
|
||||||
|
inline void useRealClock()
|
||||||
|
{
|
||||||
|
useTestClock = false;
|
||||||
|
testNowMs = 0;
|
||||||
|
clockSourceChanged = true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// Milliseconds since boot, 32-bit (wraps ~49.7 days). Drop-in for millis().
|
||||||
|
uint32_t getMillis();
|
||||||
|
|
||||||
|
/// Milliseconds since boot, 64-bit, rollover-immune. Must be polled at least once per ~49.7-day
|
||||||
|
/// wrap window to catch every wrap, and keeps mutable static carry state, so it is NOT ISR-safe.
|
||||||
|
uint64_t getMillis64();
|
||||||
|
|
||||||
|
} // namespace Time
|
||||||
@@ -37,9 +37,9 @@ ScanI2C::FoundDevice ScanI2C::firstKeyboard() const
|
|||||||
|
|
||||||
ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const
|
ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const
|
||||||
{
|
{
|
||||||
ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX,
|
ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, SC7A20, BMA423, LSM6DS3, BMX160, STK8BAXX,
|
||||||
ICM20948, QMA6100P, BMM150, BMI270, ICM42607P, ISM330DHCX};
|
ICM20948, BMM150, BMI270, ICM42607P, ISM330DHCX, QMA6100P};
|
||||||
return firstOfOrNONE(12, types);
|
return firstOfOrNONE(13, types);
|
||||||
}
|
}
|
||||||
|
|
||||||
ScanI2C::FoundDevice ScanI2C::firstMagnetometer() const
|
ScanI2C::FoundDevice ScanI2C::firstMagnetometer() const
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ class ScanI2C
|
|||||||
QMA6100P,
|
QMA6100P,
|
||||||
MPU6050,
|
MPU6050,
|
||||||
LIS3DH,
|
LIS3DH,
|
||||||
|
SC7A20, // LIS3DH register map, different WHO_AM_I
|
||||||
BMA423,
|
BMA423,
|
||||||
BQ24295,
|
BQ24295,
|
||||||
LSM6DS3,
|
LSM6DS3,
|
||||||
|
|||||||
@@ -691,6 +691,10 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
|||||||
if (registerValue == 0x3300 || registerValue == 0x3333) { // RAK4631 WisBlock has LIS3DH register at 0x3333
|
if (registerValue == 0x3300 || registerValue == 0x3333) { // RAK4631 WisBlock has LIS3DH register at 0x3333
|
||||||
type = LIS3DH;
|
type = LIS3DH;
|
||||||
logFoundDevice("LIS3DH", (uint8_t)addr.address);
|
logFoundDevice("LIS3DH", (uint8_t)addr.address);
|
||||||
|
} else if ((registerValue & 0xFF00) == 0x1100) {
|
||||||
|
// Silan SC7A20: LIS3DH register map, but answers 0x11 here.
|
||||||
|
type = SC7A20;
|
||||||
|
logFoundDevice("SC7A20", (uint8_t)addr.address);
|
||||||
} else {
|
} else {
|
||||||
type = BMA423;
|
type = BMA423;
|
||||||
logFoundDevice("BMA423", (uint8_t)addr.address);
|
logFoundDevice("BMA423", (uint8_t)addr.address);
|
||||||
|
|||||||
+11
-6
@@ -2,6 +2,7 @@
|
|||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#include "detect/ScanI2C.h"
|
#include "detect/ScanI2C.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
|
#include "mesh/MeshService.h"
|
||||||
#include "modules/NodeInfoModule.h"
|
#include "modules/NodeInfoModule.h"
|
||||||
#include <Throttle.h>
|
#include <Throttle.h>
|
||||||
#include <sys/time.h>
|
#include <sys/time.h>
|
||||||
@@ -17,12 +18,16 @@ uint32_t lastSetFromPhoneNtpOrGps = 0;
|
|||||||
static uint32_t lastTimeValidationWarning = 0;
|
static uint32_t lastTimeValidationWarning = 0;
|
||||||
static const uint32_t TIME_VALIDATION_WARNING_INTERVAL_MS = 15000; // 15 seconds
|
static const uint32_t TIME_VALIDATION_WARNING_INTERVAL_MS = 15000; // 15 seconds
|
||||||
|
|
||||||
static void triggerNodeInfoCheckOnTimeSource(RTCQuality oldQuality, RTCQuality newQuality)
|
static void onTimeSourceQualityChanged(RTCQuality oldQuality, RTCQuality newQuality)
|
||||||
{
|
{
|
||||||
if (oldQuality == RTCQualityNone && newQuality > RTCQualityNone && nodeInfoModule) {
|
if (oldQuality == RTCQualityNone && newQuality > RTCQualityNone && nodeInfoModule) {
|
||||||
LOG_DEBUG("Time source acquired (%s -> %s), triggering NodeInfo recheck", RtcName(oldQuality), RtcName(newQuality));
|
LOG_DEBUG("Time source acquired (%s -> %s), triggering NodeInfo recheck", RtcName(oldQuality), RtcName(newQuality));
|
||||||
nodeInfoModule->triggerImmediateNodeInfoCheck();
|
nodeInfoModule->triggerImmediateNodeInfoCheck();
|
||||||
}
|
}
|
||||||
|
if (oldQuality < RTCQualityFromNet && newQuality >= RTCQualityFromNet && service) {
|
||||||
|
LOG_DEBUG("RTC net quality reached (%s -> %s), reconciling rx_time", RtcName(oldQuality), RtcName(newQuality));
|
||||||
|
service->reconcilePendingRxTimes();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RTCQuality getRTCQuality()
|
RTCQuality getRTCQuality()
|
||||||
@@ -128,7 +133,7 @@ RTCSetResult readFromRTC()
|
|||||||
timeStartMsec = now;
|
timeStartMsec = now;
|
||||||
zeroOffsetSecs = tv.tv_sec;
|
zeroOffsetSecs = tv.tv_sec;
|
||||||
currentQuality = RTCQualityDevice;
|
currentQuality = RTCQualityDevice;
|
||||||
triggerNodeInfoCheckOnTimeSource(oldQuality, currentQuality);
|
onTimeSourceQualityChanged(oldQuality, currentQuality);
|
||||||
}
|
}
|
||||||
return RTCSetResultSuccess;
|
return RTCSetResultSuccess;
|
||||||
} else {
|
} else {
|
||||||
@@ -174,7 +179,7 @@ RTCSetResult readFromRTC()
|
|||||||
timeStartMsec = now;
|
timeStartMsec = now;
|
||||||
zeroOffsetSecs = tv.tv_sec;
|
zeroOffsetSecs = tv.tv_sec;
|
||||||
currentQuality = RTCQualityDevice;
|
currentQuality = RTCQualityDevice;
|
||||||
triggerNodeInfoCheckOnTimeSource(oldQuality, currentQuality);
|
onTimeSourceQualityChanged(oldQuality, currentQuality);
|
||||||
}
|
}
|
||||||
return RTCSetResultSuccess;
|
return RTCSetResultSuccess;
|
||||||
} else {
|
} else {
|
||||||
@@ -210,7 +215,7 @@ RTCSetResult readFromRTC()
|
|||||||
timeStartMsec = now;
|
timeStartMsec = now;
|
||||||
zeroOffsetSecs = tv.tv_sec;
|
zeroOffsetSecs = tv.tv_sec;
|
||||||
currentQuality = RTCQualityDevice;
|
currentQuality = RTCQualityDevice;
|
||||||
triggerNodeInfoCheckOnTimeSource(oldQuality, currentQuality);
|
onTimeSourceQualityChanged(oldQuality, currentQuality);
|
||||||
}
|
}
|
||||||
return RTCSetResultSuccess;
|
return RTCSetResultSuccess;
|
||||||
}
|
}
|
||||||
@@ -235,7 +240,7 @@ RTCSetResult readFromRTC()
|
|||||||
timeStartMsec = now;
|
timeStartMsec = now;
|
||||||
zeroOffsetSecs = tv.tv_sec;
|
zeroOffsetSecs = tv.tv_sec;
|
||||||
currentQuality = RTCQualityDevice;
|
currentQuality = RTCQualityDevice;
|
||||||
triggerNodeInfoCheckOnTimeSource(oldQuality, currentQuality);
|
onTimeSourceQualityChanged(oldQuality, currentQuality);
|
||||||
}
|
}
|
||||||
return RTCSetResultSuccess;
|
return RTCSetResultSuccess;
|
||||||
}
|
}
|
||||||
@@ -381,7 +386,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
readFromRTC();
|
readFromRTC();
|
||||||
triggerNodeInfoCheckOnTimeSource(oldQuality, currentQuality);
|
onTimeSourceQualityChanged(oldQuality, currentQuality);
|
||||||
return RTCSetResultSuccess;
|
return RTCSetResultSuccess;
|
||||||
} else {
|
} else {
|
||||||
return RTCSetResultNotSet; // RTC was already set with a higher quality time
|
return RTCSetResultNotSet; // RTC was already set with a higher quality time
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#include "graphics/Backlight.h"
|
||||||
|
|
||||||
|
#if HAS_PWM_BACKLIGHT
|
||||||
|
|
||||||
|
#include "mesh/NodeDB.h"
|
||||||
|
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
bool pinConfigured = false;
|
||||||
|
|
||||||
|
// Level restored when the backlight is switched back on after being toggled off.
|
||||||
|
uint8_t lastOnLevel = PWM_BACKLIGHT_DEFAULT;
|
||||||
|
|
||||||
|
void drive(uint8_t level)
|
||||||
|
{
|
||||||
|
if (!pinConfigured) {
|
||||||
|
pinMode(PIN_PWM_BACKLIGHT, OUTPUT);
|
||||||
|
pinConfigured = true;
|
||||||
|
}
|
||||||
|
analogWrite(PIN_PWM_BACKLIGHT, level);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void backlightSet(uint8_t level)
|
||||||
|
{
|
||||||
|
if (level > 0)
|
||||||
|
lastOnLevel = level;
|
||||||
|
uiconfig.screen_brightness = level;
|
||||||
|
drive(level);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t backlightGet()
|
||||||
|
{
|
||||||
|
return uiconfig.screen_brightness;
|
||||||
|
}
|
||||||
|
|
||||||
|
void backlightOn()
|
||||||
|
{
|
||||||
|
drive(uiconfig.screen_brightness);
|
||||||
|
}
|
||||||
|
|
||||||
|
void backlightOff()
|
||||||
|
{
|
||||||
|
drive(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void backlightToggle()
|
||||||
|
{
|
||||||
|
backlightSet(uiconfig.screen_brightness > 0 ? 0 : lastOnLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
void backlightStepUp()
|
||||||
|
{
|
||||||
|
uint16_t raised = (uint16_t)uiconfig.screen_brightness + PWM_BACKLIGHT_STEP;
|
||||||
|
backlightSet(raised > PWM_BACKLIGHT_MAX ? PWM_BACKLIGHT_MAX : (uint8_t)raised);
|
||||||
|
}
|
||||||
|
|
||||||
|
void backlightStepDown()
|
||||||
|
{
|
||||||
|
// Leave an off backlight off; otherwise clamp at the minimum.
|
||||||
|
if (uiconfig.screen_brightness == 0)
|
||||||
|
return;
|
||||||
|
backlightSet(uiconfig.screen_brightness <= PWM_BACKLIGHT_MIN + PWM_BACKLIGHT_STEP
|
||||||
|
? PWM_BACKLIGHT_MIN
|
||||||
|
: uiconfig.screen_brightness - PWM_BACKLIGHT_STEP);
|
||||||
|
}
|
||||||
|
} // namespace graphics
|
||||||
|
|
||||||
|
#endif // HAS_PWM_BACKLIGHT
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
// PWM backlight control. A variant opts in by defining PIN_PWM_BACKLIGHT, optionally with
|
||||||
|
// PWM_BACKLIGHT_DEFAULT, _MIN, _MAX and _STEP. Levels are 0..255 in uiconfig.screen_brightness.
|
||||||
|
|
||||||
|
#if defined(PIN_PWM_BACKLIGHT)
|
||||||
|
#define HAS_PWM_BACKLIGHT 1
|
||||||
|
#else
|
||||||
|
#define HAS_PWM_BACKLIGHT 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if HAS_PWM_BACKLIGHT
|
||||||
|
|
||||||
|
#ifndef PWM_BACKLIGHT_DEFAULT
|
||||||
|
#define PWM_BACKLIGHT_DEFAULT 128
|
||||||
|
#endif
|
||||||
|
#ifndef PWM_BACKLIGHT_MIN
|
||||||
|
#define PWM_BACKLIGHT_MIN 8
|
||||||
|
#endif
|
||||||
|
#ifndef PWM_BACKLIGHT_MAX
|
||||||
|
#define PWM_BACKLIGHT_MAX 248
|
||||||
|
#endif
|
||||||
|
#ifndef PWM_BACKLIGHT_STEP
|
||||||
|
#define PWM_BACKLIGHT_STEP 20
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
void backlightSet(uint8_t level);
|
||||||
|
|
||||||
|
uint8_t backlightGet();
|
||||||
|
|
||||||
|
void backlightOn(); // drive the stored level
|
||||||
|
|
||||||
|
void backlightOff(); // drive 0, leaving the stored level alone
|
||||||
|
|
||||||
|
void backlightToggle();
|
||||||
|
|
||||||
|
void backlightStepUp();
|
||||||
|
|
||||||
|
void backlightStepDown();
|
||||||
|
} // namespace graphics
|
||||||
|
|
||||||
|
#endif // HAS_PWM_BACKLIGHT
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
#include "graphics/Backlight.h"
|
||||||
|
|
||||||
#if defined(USE_EINK) && !defined(USE_EINK_PARALLELDISPLAY)
|
#if defined(USE_EINK) && !defined(USE_EINK_PARALLELDISPLAY)
|
||||||
#include "EInkDisplay2.h"
|
#include "EInkDisplay2.h"
|
||||||
@@ -158,13 +159,14 @@ bool EInkDisplay::connect()
|
|||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(TTGO_T_ECHO) || defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE) || defined(TTGO_T_ECHO_PLUS)
|
#if defined(TTGO_T_ECHO) || defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE) || defined(TTGO_T_ECHO_PLUS) || \
|
||||||
|
defined(ELECROW_ThinkNode_M8)
|
||||||
{
|
{
|
||||||
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, SPI1);
|
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, SPI1);
|
||||||
|
|
||||||
adafruitDisplay = new GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT>(*lowLevel);
|
adafruitDisplay = new GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT>(*lowLevel);
|
||||||
adafruitDisplay->init();
|
adafruitDisplay->init();
|
||||||
#if defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE)
|
#if defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE) || defined(ELECROW_ThinkNode_M8)
|
||||||
adafruitDisplay->setRotation(4);
|
adafruitDisplay->setRotation(4);
|
||||||
#else
|
#else
|
||||||
adafruitDisplay->setRotation(3);
|
adafruitDisplay->setRotation(3);
|
||||||
|
|||||||
+29
-8
@@ -61,6 +61,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#include "error.h"
|
#include "error.h"
|
||||||
#include "gps/GeoCoord.h"
|
#include "gps/GeoCoord.h"
|
||||||
#include "gps/RTC.h"
|
#include "gps/RTC.h"
|
||||||
|
#include "graphics/Backlight.h"
|
||||||
#include "graphics/ScreenFonts.h"
|
#include "graphics/ScreenFonts.h"
|
||||||
#include "graphics/SharedUIDisplay.h"
|
#include "graphics/SharedUIDisplay.h"
|
||||||
#include "graphics/TFTPalette.h"
|
#include "graphics/TFTPalette.h"
|
||||||
@@ -691,7 +692,9 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
|||||||
dispdev->displayOn();
|
dispdev->displayOn();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef PIN_EINK_EN
|
#if HAS_PWM_BACKLIGHT
|
||||||
|
graphics::backlightOn();
|
||||||
|
#elif defined(PIN_EINK_EN)
|
||||||
if (uiconfig.screen_brightness == 1)
|
if (uiconfig.screen_brightness == 1)
|
||||||
digitalWrite(PIN_EINK_EN, HIGH);
|
digitalWrite(PIN_EINK_EN, HIGH);
|
||||||
#elif defined(PCA_PIN_EINK_EN)
|
#elif defined(PCA_PIN_EINK_EN)
|
||||||
@@ -751,7 +754,9 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
|||||||
drawLockdownLockScreen(dispdev);
|
drawLockdownLockScreen(dispdev);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef PIN_EINK_EN
|
#if HAS_PWM_BACKLIGHT
|
||||||
|
graphics::backlightOff();
|
||||||
|
#elif defined(PIN_EINK_EN)
|
||||||
digitalWrite(PIN_EINK_EN, LOW);
|
digitalWrite(PIN_EINK_EN, LOW);
|
||||||
#elif defined(PCA_PIN_EINK_EN)
|
#elif defined(PCA_PIN_EINK_EN)
|
||||||
io.digitalWrite(PCA_PIN_EINK_EN, LOW);
|
io.digitalWrite(PCA_PIN_EINK_EN, LOW);
|
||||||
@@ -1848,8 +1853,12 @@ void Screen::handleStartFirmwareUpdateScreen()
|
|||||||
|
|
||||||
void Screen::increaseBrightness()
|
void Screen::increaseBrightness()
|
||||||
{
|
{
|
||||||
|
#if HAS_PWM_BACKLIGHT
|
||||||
|
graphics::backlightStepUp();
|
||||||
|
brightness = graphics::backlightGet();
|
||||||
|
#else
|
||||||
brightness = ((brightness + 62) > 254) ? brightness : (brightness + 62);
|
brightness = ((brightness + 62) > 254) ? brightness : (brightness + 62);
|
||||||
|
#endif
|
||||||
#if defined(ST7789_CS)
|
#if defined(ST7789_CS)
|
||||||
// run the setDisplayBrightness function. This works on t-decks
|
// run the setDisplayBrightness function. This works on t-decks
|
||||||
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
|
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
|
||||||
@@ -1860,8 +1869,12 @@ void Screen::increaseBrightness()
|
|||||||
|
|
||||||
void Screen::decreaseBrightness()
|
void Screen::decreaseBrightness()
|
||||||
{
|
{
|
||||||
|
#if HAS_PWM_BACKLIGHT
|
||||||
|
graphics::backlightStepDown();
|
||||||
|
brightness = graphics::backlightGet();
|
||||||
|
#else
|
||||||
brightness = (brightness < 70) ? brightness : (brightness - 62);
|
brightness = (brightness < 70) ? brightness : (brightness - 62);
|
||||||
|
#endif
|
||||||
#if defined(ST7789_CS)
|
#if defined(ST7789_CS)
|
||||||
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
|
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
|
||||||
#endif
|
#endif
|
||||||
@@ -2057,10 +2070,18 @@ int Screen::handleInputEvent(const InputEvent *event)
|
|||||||
}
|
}
|
||||||
|
|
||||||
#ifdef USE_EINK // the screen is the last input handler, so if an event makes it here, we can assume it will prompt a screen draw.
|
#ifdef USE_EINK // the screen is the last input handler, so if an event makes it here, we can assume it will prompt a screen draw.
|
||||||
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please
|
#if HAS_PWM_BACKLIGHT
|
||||||
EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update
|
// A PWM backlight change leaves the frame identical, and the refresh is blocking.
|
||||||
handleSetOn(true); // Ensure power-on to receive deep-sleep screensaver (PowerFSM should handle?)
|
const bool backlightOnly =
|
||||||
setFastFramerate(); // Draw ASAP
|
event->kbchar == INPUT_BROKER_MSG_BRIGHTNESS_UP || event->kbchar == INPUT_BROKER_MSG_BRIGHTNESS_DOWN;
|
||||||
|
if (!backlightOnly)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please
|
||||||
|
EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update
|
||||||
|
handleSetOn(true); // Ensure power-on to receive deep-sleep screensaver (PowerFSM should handle?)
|
||||||
|
setFastFramerate(); // Draw ASAP
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
if (NotificationRenderer::isOverlayBannerShowing()) {
|
if (NotificationRenderer::isOverlayBannerShowing()) {
|
||||||
NotificationRenderer::inEvent = *event;
|
NotificationRenderer::inEvent = *event;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "MessageStore.h"
|
#include "MessageStore.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "buzz.h"
|
#include "buzz.h"
|
||||||
|
#include "graphics/Backlight.h"
|
||||||
#include "graphics/Screen.h"
|
#include "graphics/Screen.h"
|
||||||
#include "graphics/SharedUIDisplay.h"
|
#include "graphics/SharedUIDisplay.h"
|
||||||
#include "graphics/TFTColorRegions.h"
|
#include "graphics/TFTColorRegions.h"
|
||||||
@@ -227,6 +228,16 @@ static void applyLoraRegion(meshtastic_Config_LoRaConfig_RegionCode region, bool
|
|||||||
|
|
||||||
void menuHandler::LoraRegionPicker(uint32_t duration)
|
void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||||
{
|
{
|
||||||
|
#ifdef HAS_HAM_2M_ONLY
|
||||||
|
// Hardware is restricted to the amateur 2m band - offer only the 2m regions
|
||||||
|
// so the user cannot pick a sub-GHz region the RF path cannot emit or receive.
|
||||||
|
static const LoraRegionOption regionOptions[] = {
|
||||||
|
{"Back", OptionsAction::Back},
|
||||||
|
{"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},
|
||||||
|
};
|
||||||
|
#else
|
||||||
static const LoraRegionOption regionOptions[] = {
|
static const LoraRegionOption regionOptions[] = {
|
||||||
{"Back", OptionsAction::Back},
|
{"Back", OptionsAction::Back},
|
||||||
{"US", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_US},
|
{"US", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_US},
|
||||||
@@ -265,6 +276,7 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
|||||||
{"ITU3_70CM (430-450)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU3_70CM},
|
{"ITU3_70CM (430-450)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU3_70CM},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
constexpr size_t regionCount = sizeof(regionOptions) / sizeof(regionOptions[0]);
|
constexpr size_t regionCount = sizeof(regionOptions) / sizeof(regionOptions[0]);
|
||||||
static std::array<const char *, regionCount> regionLabels{};
|
static std::array<const char *, regionCount> regionLabels{};
|
||||||
@@ -1097,7 +1109,7 @@ void menuHandler::homeBaseMenu()
|
|||||||
}
|
}
|
||||||
optionsEnumArray[options++] = Mute;
|
optionsEnumArray[options++] = Mute;
|
||||||
}
|
}
|
||||||
#if defined(PIN_EINK_EN) || defined(PCA_PIN_EINK_EN)
|
#if HAS_PWM_BACKLIGHT || defined(PIN_EINK_EN) || defined(PCA_PIN_EINK_EN)
|
||||||
optionsArray[options] = "Toggle Backlight";
|
optionsArray[options] = "Toggle Backlight";
|
||||||
optionsEnumArray[options++] = Backlight;
|
optionsEnumArray[options++] = Backlight;
|
||||||
#else
|
#else
|
||||||
@@ -1127,7 +1139,10 @@ void menuHandler::homeBaseMenu()
|
|||||||
}
|
}
|
||||||
} else if (selected == Backlight) {
|
} else if (selected == Backlight) {
|
||||||
screen->setOn(false);
|
screen->setOn(false);
|
||||||
#if defined(PIN_EINK_EN)
|
#if HAS_PWM_BACKLIGHT
|
||||||
|
graphics::backlightToggle();
|
||||||
|
saveUIConfig();
|
||||||
|
#elif defined(PIN_EINK_EN)
|
||||||
if (uiconfig.screen_brightness == 1) {
|
if (uiconfig.screen_brightness == 1) {
|
||||||
uiconfig.screen_brightness = 0;
|
uiconfig.screen_brightness = 0;
|
||||||
digitalWrite(PIN_EINK_EN, LOW);
|
digitalWrite(PIN_EINK_EN, LOW);
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
|
|
||||||
#ifdef ARCH_PORTDUINO
|
#ifdef ARCH_PORTDUINO
|
||||||
#include "PortduinoGlue.h"
|
#include "PortduinoGlue.h"
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -60,6 +62,11 @@ void tftSetup(void)
|
|||||||
} else
|
} else
|
||||||
#elif defined(USE_FRAMEBUFFER)
|
#elif defined(USE_FRAMEBUFFER)
|
||||||
if (portduino_config.displayPanel == fb) {
|
if (portduino_config.displayPanel == fb) {
|
||||||
|
// Rotation from yaml Display.OffsetRotate: 1=90, 2=180, 3=270 deg
|
||||||
|
char rbuf[4];
|
||||||
|
snprintf(rbuf, sizeof(rbuf), "%d", portduino_config.displayRotate ? (portduino_config.displayOffsetRotate & 3) : 0);
|
||||||
|
if (setenv("MESHTASTIC_FB_ROTATION", rbuf, 1) != 0)
|
||||||
|
LOG_ERROR("Failed to set MESHTASTIC_FB_ROTATION, framebuffer will use its default rotation");
|
||||||
if (portduino_config.displayWidth && portduino_config.displayHeight)
|
if (portduino_config.displayWidth && portduino_config.displayHeight)
|
||||||
displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::FB, (uint16_t)portduino_config.displayWidth,
|
displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::FB, (uint16_t)portduino_config.displayWidth,
|
||||||
(uint16_t)portduino_config.displayHeight);
|
(uint16_t)portduino_config.displayHeight);
|
||||||
|
|||||||
@@ -227,7 +227,13 @@ int32_t ButtonThread::runOnce()
|
|||||||
|
|
||||||
case BUTTON_EVENT_DOUBLE_PRESSED: { // not wired in if screen detected
|
case BUTTON_EVENT_DOUBLE_PRESSED: { // not wired in if screen detected
|
||||||
LOG_INFO("Double press!");
|
LOG_INFO("Double press!");
|
||||||
|
#if defined(ELECROW_ThinkNode_M8)
|
||||||
|
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED)
|
||||||
|
config.device.buzzer_mode = meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED;
|
||||||
|
else if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_DISABLED)
|
||||||
|
config.device.buzzer_mode = meshtastic_Config_DeviceConfig_BuzzerMode_ALL_ENABLED;
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
|
#endif
|
||||||
// Reset combination tracking
|
// Reset combination tracking
|
||||||
waitingForLongPress = false;
|
waitingForLongPress = false;
|
||||||
|
|
||||||
|
|||||||
@@ -35,11 +35,12 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if HAS_BUTTON || defined(ARCH_PORTDUINO)
|
#if HAS_BUTTON || defined(ARCH_PORTDUINO)
|
||||||
|
#include "graphics/Backlight.h"
|
||||||
#include "input/ButtonThread.h"
|
#include "input/ButtonThread.h"
|
||||||
|
|
||||||
#if defined(BUTTON_PIN_TOUCH)
|
#if defined(BUTTON_PIN_TOUCH)
|
||||||
ButtonThread *TouchButtonThread = nullptr;
|
ButtonThread *TouchButtonThread = nullptr;
|
||||||
#if defined(PIN_EINK_EN)
|
#if HAS_PWM_BACKLIGHT || defined(PIN_EINK_EN)
|
||||||
static bool touchBacklightWasOn = false;
|
static bool touchBacklightWasOn = false;
|
||||||
static bool touchBacklightActive = false;
|
static bool touchBacklightActive = false;
|
||||||
#endif
|
#endif
|
||||||
@@ -241,20 +242,28 @@ void InputBroker::Init()
|
|||||||
};
|
};
|
||||||
touchConfig.singlePress = INPUT_BROKER_NONE;
|
touchConfig.singlePress = INPUT_BROKER_NONE;
|
||||||
touchConfig.longPress = INPUT_BROKER_BACK;
|
touchConfig.longPress = INPUT_BROKER_BACK;
|
||||||
#if defined(PIN_EINK_EN)
|
#if HAS_PWM_BACKLIGHT || defined(PIN_EINK_EN)
|
||||||
// Touch pad drives the backlight on devices with e-ink backlight pin
|
// Touch pad drives the backlight on devices that have one
|
||||||
touchConfig.longPress = INPUT_BROKER_NONE;
|
touchConfig.longPress = INPUT_BROKER_NONE;
|
||||||
touchConfig.suppressLeadUpSound = true;
|
touchConfig.suppressLeadUpSound = true;
|
||||||
touchConfig.onPress = []() {
|
touchConfig.onPress = []() {
|
||||||
touchBacklightWasOn = uiconfig.screen_brightness == 1;
|
touchBacklightWasOn = uiconfig.screen_brightness > 0;
|
||||||
if (!touchBacklightWasOn) {
|
if (!touchBacklightWasOn) {
|
||||||
|
#if HAS_PWM_BACKLIGHT
|
||||||
|
graphics::backlightOn();
|
||||||
|
#else
|
||||||
digitalWrite(PIN_EINK_EN, HIGH);
|
digitalWrite(PIN_EINK_EN, HIGH);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
touchBacklightActive = true;
|
touchBacklightActive = true;
|
||||||
};
|
};
|
||||||
touchConfig.onRelease = []() {
|
touchConfig.onRelease = []() {
|
||||||
if (touchBacklightActive && !touchBacklightWasOn) {
|
if (touchBacklightActive && !touchBacklightWasOn) {
|
||||||
|
#if HAS_PWM_BACKLIGHT
|
||||||
|
graphics::backlightOff();
|
||||||
|
#else
|
||||||
digitalWrite(PIN_EINK_EN, LOW);
|
digitalWrite(PIN_EINK_EN, LOW);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
touchBacklightActive = false;
|
touchBacklightActive = false;
|
||||||
};
|
};
|
||||||
@@ -327,6 +336,30 @@ void InputBroker::Init()
|
|||||||
#define BUTTON_ACTIVE_PULLUP true
|
#define BUTTON_ACTIVE_PULLUP true
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if defined(ELECROW_ThinkNode_M8)
|
||||||
|
// Rotary encoder drives the UI, so the function button keeps a fixed map.
|
||||||
|
LOG_DEBUG("ThinkNode_M8 button");
|
||||||
|
UserButtonThread = new ButtonThread("FunctionButton");
|
||||||
|
{
|
||||||
|
ButtonConfig userConfig;
|
||||||
|
userConfig.pinNumber = (uint8_t)_pinNum;
|
||||||
|
userConfig.activeLow = BUTTON_ACTIVE_LOW;
|
||||||
|
userConfig.activePullup = BUTTON_ACTIVE_PULLUP;
|
||||||
|
userConfig.pullupSense = pullup_sense;
|
||||||
|
userConfig.intRoutine = []() {
|
||||||
|
UserButtonThread->userButton.tick();
|
||||||
|
UserButtonThread->setIntervalFromNow(0);
|
||||||
|
runASAP = true;
|
||||||
|
BaseType_t higherWake = 0;
|
||||||
|
concurrency::mainDelay.interruptFromISR(&higherWake);
|
||||||
|
};
|
||||||
|
userConfig.singlePress = INPUT_BROKER_SEND_PING;
|
||||||
|
userConfig.longPress = INPUT_BROKER_SHUTDOWN;
|
||||||
|
userConfig.longPressTime = 5000;
|
||||||
|
userConfig.doublePress = INPUT_BROKER_GPS_TOGGLE;
|
||||||
|
UserButtonThread->initButton(userConfig);
|
||||||
|
}
|
||||||
|
#else
|
||||||
// Buttons. Moved here cause we need NodeDB to be initialized
|
// Buttons. Moved here cause we need NodeDB to be initialized
|
||||||
// If your variant.h has a BUTTON_PIN defined, go ahead and define BUTTON_ACTIVE_LOW and BUTTON_ACTIVE_PULLUP
|
// If your variant.h has a BUTTON_PIN defined, go ahead and define BUTTON_ACTIVE_LOW and BUTTON_ACTIVE_PULLUP
|
||||||
UserButtonThread = new ButtonThread("UserButton");
|
UserButtonThread = new ButtonThread("UserButton");
|
||||||
@@ -378,8 +411,9 @@ void InputBroker::Init()
|
|||||||
userConfigNoScreen.triplePress = INPUT_BROKER_GPS_TOGGLE;
|
userConfigNoScreen.triplePress = INPUT_BROKER_GPS_TOGGLE;
|
||||||
UserButtonThread->initButton(userConfigNoScreen);
|
UserButtonThread->initButton(userConfigNoScreen);
|
||||||
}
|
}
|
||||||
#endif
|
#endif // ELECROW_ThinkNode_M8
|
||||||
#endif
|
#endif // BUTTON_PIN
|
||||||
|
#endif // HAS_BUTTON
|
||||||
|
|
||||||
#if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER
|
#if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER
|
||||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||||
|
|||||||
@@ -56,24 +56,50 @@ int32_t RotaryEncoderInterruptBase::runOnce()
|
|||||||
if (!pressDetected && buttonPressed) {
|
if (!pressDetected && buttonPressed) {
|
||||||
pressDetected = true;
|
pressDetected = true;
|
||||||
pressStartTime = now;
|
pressStartTime = now;
|
||||||
|
pressAndTurnFired = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pressDetected) {
|
if (pressDetected) {
|
||||||
|
// Press-and-turn takes precedence over the press itself.
|
||||||
|
if (pressAndTurnEnabled() && pressAndTurnDelta.load(std::memory_order_relaxed) != 0) {
|
||||||
|
// Drain in one pass: releasing the button would discard anything left over.
|
||||||
|
// Exchange, so a detent arriving from the ISR mid-drain is not lost.
|
||||||
|
int32_t pending = pressAndTurnDelta.exchange(0, std::memory_order_relaxed);
|
||||||
|
LOG_DEBUG("Rotary event Press %s (%d detents)", pending > 0 ? "CW" : "CCW", pending);
|
||||||
|
while (pending != 0) {
|
||||||
|
bool cw = pending > 0;
|
||||||
|
InputEvent turn = {};
|
||||||
|
turn.source = this->_originName;
|
||||||
|
turn.inputEvent = INPUT_BROKER_NONE;
|
||||||
|
turn.kbchar = cw ? _pressAndTurnCw : _pressAndTurnCcw;
|
||||||
|
pending -= cw ? 1 : -1;
|
||||||
|
this->notifyObservers(&turn);
|
||||||
|
}
|
||||||
|
pressAndTurnFired = true;
|
||||||
|
}
|
||||||
|
|
||||||
uint32_t duration = now - pressStartTime;
|
uint32_t duration = now - pressStartTime;
|
||||||
if (!buttonPressed) {
|
if (!buttonPressed) {
|
||||||
// released -> if short press, send short, else already sent long
|
// released -> if short press, send short, else already sent long
|
||||||
if (duration < LONG_PRESS_DURATION && now - lastPressKeyTime >= pressDebounceMs) {
|
if (!pressAndTurnFired && duration < LONG_PRESS_DURATION && now - lastPressKeyTime >= pressDebounceMs) {
|
||||||
lastPressKeyTime = now;
|
lastPressKeyTime = now;
|
||||||
LOG_DEBUG("Rotary event Press short");
|
LOG_DEBUG("Rotary event Press short");
|
||||||
e.inputEvent = this->_eventPressed;
|
e.inputEvent = this->_eventPressed;
|
||||||
|
} else if (pressAndTurnEnabled() && !pressAndTurnFired && duration >= LONG_PRESS_DURATION &&
|
||||||
|
this->_eventPressedLong != INPUT_BROKER_NONE) {
|
||||||
|
// Held long enough and no turn came, so the long press stands
|
||||||
|
LOG_DEBUG("Rotary event Press long");
|
||||||
|
e.inputEvent = this->_eventPressedLong;
|
||||||
}
|
}
|
||||||
pressDetected = false;
|
pressDetected = false;
|
||||||
pressStartTime = 0;
|
pressStartTime = 0;
|
||||||
lastPressLongEventTime = 0;
|
lastPressLongEventTime = 0;
|
||||||
|
pressAndTurnDelta.store(0, std::memory_order_relaxed);
|
||||||
|
pressAndTurnFired = false;
|
||||||
this->action = ROTARY_ACTION_NONE;
|
this->action = ROTARY_ACTION_NONE;
|
||||||
} else if (duration >= LONG_PRESS_DURATION && this->_eventPressedLong != INPUT_BROKER_NONE &&
|
} else if (!pressAndTurnEnabled() && duration >= LONG_PRESS_DURATION &&
|
||||||
lastPressLongEventTime == 0) {
|
this->_eventPressedLong != INPUT_BROKER_NONE && lastPressLongEventTime == 0) {
|
||||||
// fire single-shot long press
|
// fire single-shot long press; press-and-turn encoders defer this to release
|
||||||
lastPressLongEventTime = now;
|
lastPressLongEventTime = now;
|
||||||
LOG_DEBUG("Rotary event Press long");
|
LOG_DEBUG("Rotary event Press long");
|
||||||
e.inputEvent = this->_eventPressedLong;
|
e.inputEvent = this->_eventPressedLong;
|
||||||
@@ -87,7 +113,7 @@ int32_t RotaryEncoderInterruptBase::runOnce()
|
|||||||
e.inputEvent = this->_eventCcw;
|
e.inputEvent = this->_eventCcw;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.inputEvent != INPUT_BROKER_NONE) {
|
if (e.inputEvent != INPUT_BROKER_NONE || e.kbchar != 0) {
|
||||||
this->notifyObservers(&e);
|
this->notifyObservers(&e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,11 +121,20 @@ int32_t RotaryEncoderInterruptBase::runOnce()
|
|||||||
this->action = ROTARY_ACTION_NONE;
|
this->action = ROTARY_ACTION_NONE;
|
||||||
} else if (now - pressStartTime < LONG_PRESS_DURATION) {
|
} else if (now - pressStartTime < LONG_PRESS_DURATION) {
|
||||||
return (20); // keep checking for long/short until time expires
|
return (20); // keep checking for long/short until time expires
|
||||||
|
} else if (pressAndTurnEnabled()) {
|
||||||
|
// Keep polling while held, rather than relying on intHandler()'s reschedule from ISR.
|
||||||
|
return (20);
|
||||||
}
|
}
|
||||||
|
|
||||||
return INT32_MAX;
|
return INT32_MAX;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RotaryEncoderInterruptBase::setPressAndTurnChars(unsigned char cw, unsigned char ccw)
|
||||||
|
{
|
||||||
|
this->_pressAndTurnCw = cw;
|
||||||
|
this->_pressAndTurnCcw = ccw;
|
||||||
|
}
|
||||||
|
|
||||||
void RotaryEncoderInterruptBase::intPressHandler()
|
void RotaryEncoderInterruptBase::intPressHandler()
|
||||||
{
|
{
|
||||||
this->action = ROTARY_ACTION_PRESSED;
|
this->action = ROTARY_ACTION_PRESSED;
|
||||||
@@ -145,7 +180,10 @@ RotaryEncoderInterruptBaseStateType RotaryEncoderInterruptBase::intHandler(bool
|
|||||||
if (actualPinRaising && (otherPinLevel == LOW)) {
|
if (actualPinRaising && (otherPinLevel == LOW)) {
|
||||||
if (state == ROTARY_EVENT_CLEARED) {
|
if (state == ROTARY_EVENT_CLEARED) {
|
||||||
newState = ROTARY_EVENT_OCCURRED;
|
newState = ROTARY_EVENT_OCCURRED;
|
||||||
if ((this->action != ROTARY_ACTION_PRESSED) && (this->action != action)) {
|
if (this->action == ROTARY_ACTION_PRESSED) {
|
||||||
|
// Turning while held; runOnce() ignores this unless press-and-turn is enabled.
|
||||||
|
pressAndTurnDelta.fetch_add((action == ROTARY_ACTION_CW) ? 1 : -1, std::memory_order_relaxed);
|
||||||
|
} else {
|
||||||
this->action = action;
|
this->action = action;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
#include "InputBroker.h"
|
#include "InputBroker.h"
|
||||||
#include "concurrency/OSThread.h"
|
#include "concurrency/OSThread.h"
|
||||||
#include "mesh/NodeDB.h"
|
#include "mesh/NodeDB.h"
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
enum RotaryEncoderInterruptBaseStateType { ROTARY_EVENT_OCCURRED, ROTARY_EVENT_CLEARED };
|
enum RotaryEncoderInterruptBaseStateType { ROTARY_EVENT_OCCURRED, ROTARY_EVENT_CLEARED };
|
||||||
|
|
||||||
enum RotaryEncoderInterruptBaseActionType { ROTARY_ACTION_NONE, ROTARY_ACTION_PRESSED, ROTARY_ACTION_CW, ROTARY_ACTION_CCW };
|
enum RotaryEncoderInterruptBaseActionType { ROTARY_ACTION_NONE, ROTARY_ACTION_PRESSED, ROTARY_ACTION_CW, ROTARY_ACTION_CCW };
|
||||||
|
|
||||||
class RotaryEncoderInterruptBase : public Observable<const InputEvent *>, public concurrency::OSThread
|
class RotaryEncoderInterruptBase : public Observable<const InputEvent *>, public concurrency::OSThread
|
||||||
@@ -20,6 +20,10 @@ class RotaryEncoderInterruptBase : public Observable<const InputEvent *>, public
|
|||||||
void intAHandler();
|
void intAHandler();
|
||||||
void intBHandler();
|
void intBHandler();
|
||||||
|
|
||||||
|
// Turning while the button is held emits `cw` / `ccw` as kbchar and suppresses the press
|
||||||
|
// event. Both default to 0, leaving rotation-while-pressed ignored.
|
||||||
|
void setPressAndTurnChars(unsigned char cw, unsigned char ccw);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual int32_t runOnce() override;
|
virtual int32_t runOnce() override;
|
||||||
RotaryEncoderInterruptBaseStateType intHandler(bool actualPinRaising, int otherPinLevel,
|
RotaryEncoderInterruptBaseStateType intHandler(bool actualPinRaising, int otherPinLevel,
|
||||||
@@ -31,6 +35,8 @@ class RotaryEncoderInterruptBase : public Observable<const InputEvent *>, public
|
|||||||
volatile int rotaryLevelA = LOW;
|
volatile int rotaryLevelA = LOW;
|
||||||
volatile int rotaryLevelB = LOW;
|
volatile int rotaryLevelB = LOW;
|
||||||
volatile RotaryEncoderInterruptBaseActionType action = ROTARY_ACTION_NONE;
|
volatile RotaryEncoderInterruptBaseActionType action = ROTARY_ACTION_NONE;
|
||||||
|
// Detents counted while the button is held: positive clockwise, negative counter-clockwise.
|
||||||
|
std::atomic<int32_t> pressAndTurnDelta{0};
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// pins and events
|
// pins and events
|
||||||
@@ -43,9 +49,16 @@ class RotaryEncoderInterruptBase : public Observable<const InputEvent *>, public
|
|||||||
input_broker_event _eventPressedLong = INPUT_BROKER_NONE;
|
input_broker_event _eventPressedLong = INPUT_BROKER_NONE;
|
||||||
const char *_originName;
|
const char *_originName;
|
||||||
|
|
||||||
|
// Press-and-turn characters, 0 when the gesture is not in use
|
||||||
|
unsigned char _pressAndTurnCw = 0;
|
||||||
|
unsigned char _pressAndTurnCcw = 0;
|
||||||
|
bool pressAndTurnEnabled() const { return _pressAndTurnCw != 0 || _pressAndTurnCcw != 0; }
|
||||||
|
|
||||||
// Long press detection variables
|
// Long press detection variables
|
||||||
uint32_t pressStartTime = 0;
|
uint32_t pressStartTime = 0;
|
||||||
bool pressDetected = false;
|
bool pressDetected = false;
|
||||||
|
// Suppresses the press event on release once press-and-turn has fired.
|
||||||
|
bool pressAndTurnFired = false;
|
||||||
uint32_t lastPressLongEventTime = 0;
|
uint32_t lastPressLongEventTime = 0;
|
||||||
unsigned long lastPressKeyTime = 0;
|
unsigned long lastPressKeyTime = 0;
|
||||||
static const uint32_t LONG_PRESS_DURATION = 300; // ms
|
static const uint32_t LONG_PRESS_DURATION = 300; // ms
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ bool RotaryEncoderInterruptImpl1::init()
|
|||||||
RotaryEncoderInterruptBase::init(pinA, pinB, pinPress, eventCw, eventCcw, eventPressed, eventPressedLong,
|
RotaryEncoderInterruptBase::init(pinA, pinB, pinPress, eventCw, eventCcw, eventPressed, eventPressedLong,
|
||||||
RotaryEncoderInterruptImpl1::handleIntA, RotaryEncoderInterruptImpl1::handleIntB,
|
RotaryEncoderInterruptImpl1::handleIntA, RotaryEncoderInterruptImpl1::handleIntB,
|
||||||
RotaryEncoderInterruptImpl1::handleIntPressed);
|
RotaryEncoderInterruptImpl1::handleIntPressed);
|
||||||
|
#if defined(INPUTDRIVER_PRESS_AND_TURN_CW) && defined(INPUTDRIVER_PRESS_AND_TURN_CCW)
|
||||||
|
setPressAndTurnChars(INPUTDRIVER_PRESS_AND_TURN_CW, INPUTDRIVER_PRESS_AND_TURN_CCW);
|
||||||
|
#endif
|
||||||
inputBroker->registerSource(this);
|
inputBroker->registerSource(this);
|
||||||
#ifndef HAS_PHYSICAL_KEYBOARD
|
#ifndef HAS_PHYSICAL_KEYBOARD
|
||||||
osk_found = true;
|
osk_found = true;
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ uint32_t Default::getConfiguredOrMinimumValue(uint32_t configured, uint32_t minV
|
|||||||
uint8_t Default::getConfiguredOrDefaultHopLimit(uint8_t configured)
|
uint8_t Default::getConfiguredOrDefaultHopLimit(uint8_t configured)
|
||||||
{
|
{
|
||||||
#if USERPREFS_EVENT_MODE
|
#if USERPREFS_EVENT_MODE
|
||||||
return (configured > HOP_RELIABLE) ? HOP_RELIABLE : config.lora.hop_limit;
|
return (configured >= eventModeHopLimit) ? eventModeHopLimit : config.lora.hop_limit;
|
||||||
#else
|
#else
|
||||||
return (configured >= HOP_MAX) ? HOP_MAX : config.lora.hop_limit;
|
return (configured >= HOP_MAX) ? HOP_MAX : config.lora.hop_limit;
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+15
-1
@@ -5,6 +5,7 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <meshUtils.h>
|
#include <meshUtils.h>
|
||||||
|
#include <type_traits>
|
||||||
#define ONE_DAY 24 * 60 * 60
|
#define ONE_DAY 24 * 60 * 60
|
||||||
#define ONE_MINUTE_MS 60 * 1000
|
#define ONE_MINUTE_MS 60 * 1000
|
||||||
#define THIRTY_SECONDS_MS 30 * 1000
|
#define THIRTY_SECONDS_MS 30 * 1000
|
||||||
@@ -75,7 +76,20 @@ enum class TrafficType { POSITION, TELEMETRY };
|
|||||||
|
|
||||||
class Default
|
class Default
|
||||||
{
|
{
|
||||||
|
#if USERPREFS_EVENT_MODE && defined(USERPREFS_EVENT_MODE_HOP_LIMIT)
|
||||||
|
static constexpr auto eventModeHopLimitSetting = USERPREFS_EVENT_MODE_HOP_LIMIT;
|
||||||
|
#else
|
||||||
|
static constexpr auto eventModeHopLimitSetting = HOP_RELIABLE;
|
||||||
|
#endif
|
||||||
|
using EventModeHopLimitType = typename std::remove_cv<decltype(eventModeHopLimitSetting)>::type;
|
||||||
|
static_assert(std::is_integral<EventModeHopLimitType>::value && !std::is_same<EventModeHopLimitType, bool>::value &&
|
||||||
|
eventModeHopLimitSetting >= 0 && eventModeHopLimitSetting <= HOP_MAX,
|
||||||
|
"USERPREFS_EVENT_MODE_HOP_LIMIT must be an integer between 0 and 7");
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
static constexpr uint8_t eventModeHopLimit = static_cast<uint8_t>(eventModeHopLimitSetting);
|
||||||
|
static constexpr uint8_t eventModeRelayHopLimit = eventModeHopLimit > 0 ? eventModeHopLimit - 1 : 0;
|
||||||
|
|
||||||
static uint32_t getConfiguredOrDefaultMs(uint32_t configuredInterval);
|
static uint32_t getConfiguredOrDefaultMs(uint32_t configuredInterval);
|
||||||
static uint32_t getConfiguredOrDefaultMs(uint32_t configuredInterval, uint32_t defaultInterval);
|
static uint32_t getConfiguredOrDefaultMs(uint32_t configuredInterval, uint32_t defaultInterval);
|
||||||
static uint32_t getConfiguredOrDefault(uint32_t configured, uint32_t defaultValue);
|
static uint32_t getConfiguredOrDefault(uint32_t configured, uint32_t defaultValue);
|
||||||
@@ -129,4 +143,4 @@ class Default
|
|||||||
return 1.0 + (nodesOverForty * throttlingFactor); // Each number of online node scales by throttle factor
|
return 1.0 + (nodesOverForty * throttlingFactor); // Each number of online node scales by throttle factor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ template class LR11x0Interface<LR1110>;
|
|||||||
template class LR11x0Interface<LR1120>;
|
template class LR11x0Interface<LR1120>;
|
||||||
template class LR11x0Interface<LR1121>;
|
template class LR11x0Interface<LR1121>;
|
||||||
#endif
|
#endif
|
||||||
#if defined(USE_LR2021) && RADIOLIB_EXCLUDE_LR2021 != 1
|
#if (defined(USE_LR2021) || defined(ARCH_PORTDUINO)) && RADIOLIB_EXCLUDE_LR2021 != 1
|
||||||
template class LR20x0Interface<LR2021>;
|
template class LR20x0Interface<LR2021>;
|
||||||
#endif
|
#endif
|
||||||
#ifdef ARCH_STM32WL
|
#ifdef ARCH_STM32WL
|
||||||
|
|||||||
@@ -247,6 +247,7 @@ template <typename T> void LR11x0Interface<T>::addReceiveMetadata(meshtastic_Mes
|
|||||||
// LOG_DEBUG("PacketStatus %x", lora.getPacketStatus());
|
// LOG_DEBUG("PacketStatus %x", lora.getPacketStatus());
|
||||||
mp->rx_snr = lora.getSNR();
|
mp->rx_snr = lora.getSNR();
|
||||||
mp->rx_rssi = lround(lora.getRSSI());
|
mp->rx_rssi = lround(lora.getRSSI());
|
||||||
|
mp->has_rx_rssi = true; // rx_rssi has explicit presence - a genuine reading must be marked present to survive encoding
|
||||||
LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError());
|
LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,4 +379,8 @@ template <typename T> int16_t LR11x0Interface<T>::getCurrentRSSI()
|
|||||||
#endif
|
#endif
|
||||||
return (int16_t)round(rssi);
|
return (int16_t)round(rssi);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't leak the aliases into the files InterfacesTemplates.cpp includes after this one.
|
||||||
|
#undef rfswitch_dio_pins
|
||||||
|
#undef rfswitch_table
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
|
||||||
#if defined(USE_LR2021) && RADIOLIB_EXCLUDE_LR2021 != 1
|
#if (defined(USE_LR2021) || defined(ARCH_PORTDUINO)) && RADIOLIB_EXCLUDE_LR2021 != 1
|
||||||
|
|
||||||
#include "LR2021Interface.h"
|
#include "LR2021Interface.h"
|
||||||
#include "error.h"
|
#include "error.h"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
|
||||||
#if defined(USE_LR2021) && RADIOLIB_EXCLUDE_LR2021 != 1
|
#if (defined(USE_LR2021) || defined(ARCH_PORTDUINO)) && RADIOLIB_EXCLUDE_LR2021 != 1
|
||||||
#include "LR20x0Interface.h"
|
#include "LR20x0Interface.h"
|
||||||
#include "error.h"
|
#include "error.h"
|
||||||
#include "mesh/NodeDB.h"
|
#include "mesh/NodeDB.h"
|
||||||
@@ -253,6 +253,7 @@ template <typename T> void LR20x0Interface<T>::addReceiveMetadata(meshtastic_Mes
|
|||||||
// LOG_DEBUG("PacketStatus %x", lora.getPacketStatus());
|
// LOG_DEBUG("PacketStatus %x", lora.getPacketStatus());
|
||||||
mp->rx_snr = lora.getSNR();
|
mp->rx_snr = lora.getSNR();
|
||||||
mp->rx_rssi = lround(lora.getRSSI());
|
mp->rx_rssi = lround(lora.getRSSI());
|
||||||
|
mp->has_rx_rssi = true; // rx_rssi has explicit presence - a genuine reading must be marked present to survive encoding
|
||||||
// LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError()); // not implemented for LR20x0, but noop for LR11x0
|
// LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError()); // not implemented for LR20x0, but noop for LR11x0
|
||||||
// too(!)
|
// too(!)
|
||||||
}
|
}
|
||||||
@@ -380,4 +381,9 @@ template <typename T> int16_t LR20x0Interface<T>::getCurrentRSSI()
|
|||||||
float rssi = lora.getRSSI(false, true);
|
float rssi = lora.getRSSI(false, true);
|
||||||
return (int16_t)round(rssi);
|
return (int16_t)round(rssi);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't leak the aliases into the files InterfacesTemplates.cpp includes after this one.
|
||||||
|
#undef lr20x0_rfswitch_dio_pins
|
||||||
|
#undef lr20x0_rfswitch_table
|
||||||
|
#undef LR20x0
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ meshtastic_MeshPacket *MeshModule::allocErrorResponse(meshtastic_Routing_Error e
|
|||||||
uint8_t channelIndex =
|
uint8_t channelIndex =
|
||||||
p->which_payload_variant == meshtastic_MeshPacket_decoded_tag ? p->channel : channels.getPrimaryIndex();
|
p->which_payload_variant == meshtastic_MeshPacket_decoded_tag ? p->channel : channels.getPrimaryIndex();
|
||||||
auto r = allocAckNak(err, getFrom(p), p->id, channelIndex);
|
auto r = allocAckNak(err, getFrom(p), p->id, channelIndex);
|
||||||
|
if (!r) // pool exhausted; callers treat a null reply as "no response"
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
setReplyTo(r, *p);
|
setReplyTo(r, *p);
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include "Power.h"
|
#include "Power.h"
|
||||||
#include "PowerFSM.h"
|
#include "PowerFSM.h"
|
||||||
#include "TypeConversions.h"
|
#include "TypeConversions.h"
|
||||||
|
#include "UptimeClock.h"
|
||||||
#include "gps/RTC.h"
|
#include "gps/RTC.h"
|
||||||
#include "graphics/draw/MessageRenderer.h"
|
#include "graphics/draw/MessageRenderer.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
@@ -180,6 +181,36 @@ NodeNum MeshService::getNodenumFromRequestId(uint32_t request_id)
|
|||||||
return nodenum;
|
return nodenum;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Back-calculate the real epoch for any queued packet still carrying a millis() rx_time
|
||||||
|
// placeholder, now that the clock is trustworthy.
|
||||||
|
void MeshService::reconcilePendingRxTimes()
|
||||||
|
{
|
||||||
|
const uint32_t nowEpoch = getValidTime(RTCQualityFromNet);
|
||||||
|
if (nowEpoch == 0) // called before the clock was actually valid - nothing to reconcile against
|
||||||
|
return;
|
||||||
|
const uint32_t nowMillis = Time::getMillis();
|
||||||
|
|
||||||
|
// Rotate the queue once. TypedQueue is strictly FIFO on both backends, so dequeueing and
|
||||||
|
// re-enqueueing every element in turn leaves the delivery order unchanged.
|
||||||
|
for (int remaining = toPhoneQueue.numUsed(); remaining > 0; remaining--) {
|
||||||
|
meshtastic_MeshPacket *p = toPhoneQueue.dequeuePtr(0);
|
||||||
|
if (!p) // drained from under us - nothing left to rotate
|
||||||
|
break;
|
||||||
|
if (!p->has_rx_time) {
|
||||||
|
// Unsigned subtraction is wraparound-safe; rx_time is a 32-bit wire field, so the
|
||||||
|
// placeholder was never wider than 32 bits to begin with.
|
||||||
|
const uint32_t elapsedMs = nowMillis - p->rx_time;
|
||||||
|
p->rx_time = nowEpoch - (elapsedMs / 1000);
|
||||||
|
p->has_rx_time = true;
|
||||||
|
}
|
||||||
|
if (!toPhoneQueue.enqueue(p, 0)) { // mirrors sendToPhone()'s degrade-on-failure path
|
||||||
|
LOG_CRIT("Failed to requeue a packet into toPhoneQueue!");
|
||||||
|
releaseToPool(p);
|
||||||
|
fromNum++; // notify observers so the phone can resync
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#if MESHTASTIC_ENABLE_FRAME_INJECTION
|
#if MESHTASTIC_ENABLE_FRAME_INJECTION
|
||||||
// Deliver a client-supplied frame into the receive pipeline as if it arrived off the LoRa chip. Mirrors
|
// Deliver a client-supplied frame into the receive pipeline as if it arrived off the LoRa chip. Mirrors
|
||||||
// the portduino SimRadio SIMULATOR_APP unwrap so the same host wire format works on real hardware: the
|
// the portduino SimRadio SIMULATOR_APP unwrap so the same host wire format works on real hardware: the
|
||||||
@@ -216,9 +247,13 @@ void MeshService::injectAsReceived(meshtastic_MeshPacket &p)
|
|||||||
return;
|
return;
|
||||||
if (mp->rx_snr == 0) // plausible synthetic link metadata unless the caller set it
|
if (mp->rx_snr == 0) // plausible synthetic link metadata unless the caller set it
|
||||||
mp->rx_snr = 8;
|
mp->rx_snr = 8;
|
||||||
if (mp->rx_rssi == 0)
|
if (!mp->has_rx_rssi) { // rx_rssi has explicit presence; only fabricate if the caller didn't supply a real one
|
||||||
mp->rx_rssi = -40;
|
mp->rx_rssi = -40;
|
||||||
mp->rx_time = getValidTime(RTCQualityFromNet);
|
mp->has_rx_rssi = true;
|
||||||
|
}
|
||||||
|
// dispatchReceived() restamps this when the packet re-enters the pipeline below; stamp it
|
||||||
|
// here anyway so the packet is never observable with an unset arrival time.
|
||||||
|
stampRxTime(mp);
|
||||||
LOG_INFO("inject: RX from=0x%08x to=0x%08x id=0x%08x ch=%d %s", mp->from, mp->to, mp->id, mp->channel,
|
LOG_INFO("inject: RX from=0x%08x to=0x%08x id=0x%08x ch=%d %s", mp->from, mp->to, mp->id, mp->channel,
|
||||||
mp->which_payload_variant == meshtastic_MeshPacket_encrypted_tag ? "encrypted" : "decoded");
|
mp->which_payload_variant == meshtastic_MeshPacket_encrypted_tag ? "encrypted" : "decoded");
|
||||||
router->enqueueReceivedMessage(mp);
|
router->enqueueReceivedMessage(mp);
|
||||||
@@ -256,7 +291,8 @@ void MeshService::handleToRadio(meshtastic_MeshPacket &p)
|
|||||||
if (p.id == 0)
|
if (p.id == 0)
|
||||||
p.id = generatePacketId(); // If the phone didn't supply one, then pick one
|
p.id = generatePacketId(); // If the phone didn't supply one, then pick one
|
||||||
|
|
||||||
p.rx_time = getValidTime(RTCQualityFromNet); // Record the time the packet arrived from the phone
|
// Record the time the packet arrived from the phone.
|
||||||
|
stampRxTime(&p);
|
||||||
|
|
||||||
IF_SCREEN(if (p.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP && p.decoded.payload.size > 0 &&
|
IF_SCREEN(if (p.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP && p.decoded.payload.size > 0 &&
|
||||||
p.to != NODENUM_BROADCAST && p.to != 0) // DM only
|
p.to != NODENUM_BROADCAST && p.to != 0) // DM only
|
||||||
@@ -593,6 +629,11 @@ bool MeshService::isToPhoneQueueEmpty()
|
|||||||
|
|
||||||
uint32_t MeshService::GetTimeSinceMeshPacket(const meshtastic_MeshPacket *mp)
|
uint32_t MeshService::GetTimeSinceMeshPacket(const meshtastic_MeshPacket *mp)
|
||||||
{
|
{
|
||||||
|
// rx_time may be a millis() placeholder while has_rx_time is false - don't age it as
|
||||||
|
// wall-clock, and don't pass it off as "just now" either.
|
||||||
|
if (!mp->has_rx_time)
|
||||||
|
return SINCE_UNKNOWN;
|
||||||
|
|
||||||
uint32_t now = getTime();
|
uint32_t now = getTime();
|
||||||
|
|
||||||
uint32_t last_seen = mp->rx_time;
|
uint32_t last_seen = mp->rx_time;
|
||||||
|
|||||||
@@ -137,6 +137,10 @@ class MeshService
|
|||||||
// search the queue for a request id and return the matching nodenum
|
// search the queue for a request id and return the matching nodenum
|
||||||
NodeNum getNodenumFromRequestId(uint32_t request_id);
|
NodeNum getNodenumFromRequestId(uint32_t request_id);
|
||||||
|
|
||||||
|
// Rewrite any queued-for-phone packet still carrying a millis() rx_time placeholder into a
|
||||||
|
// real epoch, now that the wall clock is trustworthy.
|
||||||
|
void reconcilePendingRxTimes();
|
||||||
|
|
||||||
// Release QueueStatus packet to pool
|
// Release QueueStatus packet to pool
|
||||||
void releaseQueueStatusToPool(meshtastic_QueueStatus *p) { queueStatusPool.release(p); }
|
void releaseQueueStatusToPool(meshtastic_QueueStatus *p) { queueStatusPool.release(p); }
|
||||||
|
|
||||||
@@ -205,6 +209,7 @@ class MeshService
|
|||||||
|
|
||||||
ErrorCode sendQueueStatusToPhone(const meshtastic_QueueStatus &qs, ErrorCode res, uint32_t mesh_packet_id);
|
ErrorCode sendQueueStatusToPhone(const meshtastic_QueueStatus &qs, ErrorCode res, uint32_t mesh_packet_id);
|
||||||
|
|
||||||
|
/// Seconds since the packet arrived, or SINCE_UNKNOWN if it carries no trustworthy rx_time.
|
||||||
uint32_t GetTimeSinceMeshPacket(const meshtastic_MeshPacket *mp);
|
uint32_t GetTimeSinceMeshPacket(const meshtastic_MeshPacket *mp);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "NextHopRouter.h"
|
#include "NextHopRouter.h"
|
||||||
|
#include "Default.h"
|
||||||
#include "MeshTypes.h"
|
#include "MeshTypes.h"
|
||||||
#include "meshUtils.h"
|
#include "meshUtils.h"
|
||||||
#if !MESHTASTIC_EXCLUDE_TRACEROUTE
|
#if !MESHTASTIC_EXCLUDE_TRACEROUTE
|
||||||
@@ -9,6 +10,18 @@
|
|||||||
#endif
|
#endif
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
|
|
||||||
|
#if USERPREFS_EVENT_MODE
|
||||||
|
static void capEventRelayHops(meshtastic_MeshPacket *packet)
|
||||||
|
{
|
||||||
|
if (packet->hop_limit <= Default::eventModeRelayHopLimit)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const uint8_t reduction = packet->hop_limit - Default::eventModeRelayHopLimit;
|
||||||
|
packet->hop_start = reduction <= packet->hop_start ? packet->hop_start - reduction : 0;
|
||||||
|
packet->hop_limit = Default::eventModeRelayHopLimit;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
NextHopRouter::NextHopRouter() {}
|
NextHopRouter::NextHopRouter() {}
|
||||||
|
|
||||||
bool NextHopRouter::relayOpaquePacket(const meshtastic_MeshPacket *p)
|
bool NextHopRouter::relayOpaquePacket(const meshtastic_MeshPacket *p)
|
||||||
@@ -26,8 +39,16 @@ bool NextHopRouter::relayOpaquePacket(const meshtastic_MeshPacket *p)
|
|||||||
if (!relay)
|
if (!relay)
|
||||||
return false;
|
return false;
|
||||||
relay->hop_limit--;
|
relay->hop_limit--;
|
||||||
|
#if USERPREFS_EVENT_MODE
|
||||||
|
capEventRelayHops(relay);
|
||||||
|
#endif
|
||||||
relay->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum());
|
relay->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum());
|
||||||
return Router::send(relay) == ERRNO_OK;
|
// The interface declines some packets (NODENUM_BROADCAST_NO_LORA) with ERRNO_SHOULD_RELEASE,
|
||||||
|
// which leaves the copy ours to free. Dropping it here would leak a pool slot per opaque frame.
|
||||||
|
ErrorCode res = Router::send(relay);
|
||||||
|
if (res == ERRNO_SHOULD_RELEASE)
|
||||||
|
packetPool.release(relay);
|
||||||
|
return res == ERRNO_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
PendingPacket::PendingPacket(meshtastic_MeshPacket *p, uint8_t numRetransmissions)
|
PendingPacket::PendingPacket(meshtastic_MeshPacket *p, uint8_t numRetransmissions)
|
||||||
@@ -206,11 +227,7 @@ bool NextHopRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p)
|
|||||||
LOG_INFO("favorite-ROUTER/CLIENT_BASE-to-ROUTER/CLIENT_BASE rebroadcast: preserving hop_limit");
|
LOG_INFO("favorite-ROUTER/CLIENT_BASE-to-ROUTER/CLIENT_BASE rebroadcast: preserving hop_limit");
|
||||||
}
|
}
|
||||||
#if USERPREFS_EVENT_MODE
|
#if USERPREFS_EVENT_MODE
|
||||||
if (tosend->hop_limit > 2) {
|
capEventRelayHops(tosend);
|
||||||
// if we are "correcting" the hop_limit, "correct" the hop_start by the same amount to preserve hops away.
|
|
||||||
tosend->hop_start -= (tosend->hop_limit - 2);
|
|
||||||
tosend->hop_limit = 2;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
ErrorCode res =
|
ErrorCode res =
|
||||||
|
|||||||
+73
-15
@@ -202,7 +202,9 @@ bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostre
|
|||||||
if (ostream) {
|
if (ostream) {
|
||||||
const auto *vec = static_cast<const std::vector<meshtastic_NodeInfoLite> *>(iter->pData);
|
const auto *vec = static_cast<const std::vector<meshtastic_NodeInfoLite> *>(iter->pData);
|
||||||
for (auto item : *vec) {
|
for (auto item : *vec) {
|
||||||
item.snr_q4 = (int32_t)(item.snr * 4.0f);
|
// Round rather than truncate: truncation wiped any |SNR| < 0.25 dB to exactly
|
||||||
|
// 0, which collided with the "never stored" sentinel below.
|
||||||
|
item.snr_q4 = (int32_t)lroundf(item.snr * 4.0f);
|
||||||
item.snr = 0.0f;
|
item.snr = 0.0f;
|
||||||
if (!pb_encode_tag_for_field(ostream, iter))
|
if (!pb_encode_tag_for_field(ostream, iter))
|
||||||
return false;
|
return false;
|
||||||
@@ -214,8 +216,14 @@ bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostre
|
|||||||
meshtastic_NodeInfoLite node = meshtastic_NodeInfoLite_init_zero;
|
meshtastic_NodeInfoLite node = meshtastic_NodeInfoLite_init_zero;
|
||||||
auto *vec = static_cast<std::vector<meshtastic_NodeInfoLite> *>(iter->pData);
|
auto *vec = static_cast<std::vector<meshtastic_NodeInfoLite> *>(iter->pData);
|
||||||
if (pb_decode(istream, meshtastic_NodeInfoLite_fields, &node)) {
|
if (pb_decode(istream, meshtastic_NodeInfoLite_fields, &node)) {
|
||||||
if (node.snr_q4)
|
// snr_q4 = 0 is byte-identical to "field never written" but 0 dB is valid.
|
||||||
|
// NODEINFO_BITFIELD_HAS_SNR_MASK disambiguates going forward; legacy
|
||||||
|
// records (bit clear) treat this as unknown.
|
||||||
|
if (nodeInfoLiteHasSnr(&node)) {
|
||||||
node.snr = node.snr_q4 / 4.0f;
|
node.snr = node.snr_q4 / 4.0f;
|
||||||
|
} else if (node.snr_q4) {
|
||||||
|
node.snr = node.snr_q4 / 4.0f;
|
||||||
|
}
|
||||||
node.snr_q4 = 0;
|
node.snr_q4 = 0;
|
||||||
vec->push_back(node);
|
vec->push_back(node);
|
||||||
}
|
}
|
||||||
@@ -733,6 +741,10 @@ void NodeDB::resetRadioConfig(bool is_fresh_install)
|
|||||||
LOG_INFO("Set default channel and radio preferences!");
|
LOG_INFO("Set default channel and radio preferences!");
|
||||||
|
|
||||||
channels.initDefaults();
|
channels.initDefaults();
|
||||||
|
// Defaults ship the public PSK, so strip it again before onConfigChanged() publishes hashes;
|
||||||
|
// loadFromDisk's sanitation is a no-op when the channel file was absent or corrupt.
|
||||||
|
if (owner.is_licensed)
|
||||||
|
channels.ensureLicensedOperation();
|
||||||
}
|
}
|
||||||
|
|
||||||
channels.onConfigChanged();
|
channels.onConfigChanged();
|
||||||
@@ -919,7 +931,11 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
|
|||||||
config.lora.override_frequency = USERPREFS_LORACONFIG_OVERRIDE_FREQUENCY;
|
config.lora.override_frequency = USERPREFS_LORACONFIG_OVERRIDE_FREQUENCY;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if USERPREFS_EVENT_MODE
|
||||||
|
config.lora.hop_limit = Default::eventModeHopLimit;
|
||||||
|
#else
|
||||||
config.lora.hop_limit = HOP_RELIABLE;
|
config.lora.hop_limit = HOP_RELIABLE;
|
||||||
|
#endif
|
||||||
#ifdef USERPREFS_CONFIG_LORA_IGNORE_MQTT
|
#ifdef USERPREFS_CONFIG_LORA_IGNORE_MQTT
|
||||||
config.lora.ignore_mqtt = USERPREFS_CONFIG_LORA_IGNORE_MQTT;
|
config.lora.ignore_mqtt = USERPREFS_CONFIG_LORA_IGNORE_MQTT;
|
||||||
#else
|
#else
|
||||||
@@ -1254,6 +1270,15 @@ void NodeDB::installDefaultModuleConfig()
|
|||||||
moduleConfig.external_notification.active = true;
|
moduleConfig.external_notification.active = true;
|
||||||
#endif // NANO_G2_ULTRA
|
#endif // NANO_G2_ULTRA
|
||||||
|
|
||||||
|
#ifdef ELECROW_ThinkNode_M8
|
||||||
|
moduleConfig.canned_message.rotary1_enabled = true;
|
||||||
|
moduleConfig.canned_message.inputbroker_pin_a = PIN_BUTTON_EC04_A;
|
||||||
|
moduleConfig.canned_message.inputbroker_pin_b = PIN_BUTTON_EC04_B;
|
||||||
|
moduleConfig.canned_message.inputbroker_pin_press = PIN_BUTTON_EC04;
|
||||||
|
moduleConfig.canned_message.inputbroker_event_cw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT;
|
||||||
|
moduleConfig.canned_message.inputbroker_event_ccw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT;
|
||||||
|
moduleConfig.canned_message.inputbroker_event_press = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT;
|
||||||
|
#endif
|
||||||
#ifdef T_LORA_PAGER
|
#ifdef T_LORA_PAGER
|
||||||
moduleConfig.canned_message.updown1_enabled = true;
|
moduleConfig.canned_message.updown1_enabled = true;
|
||||||
moduleConfig.canned_message.inputbroker_pin_a = ROTARY_A;
|
moduleConfig.canned_message.inputbroker_pin_a = ROTARY_A;
|
||||||
@@ -1591,15 +1616,19 @@ void NodeDB::resetNodes(bool keepFavorites)
|
|||||||
numMeshNodes = 1;
|
numMeshNodes = 1;
|
||||||
if (keepFavorites) {
|
if (keepFavorites) {
|
||||||
LOG_INFO("Clearing node database - preserving favorites");
|
LOG_INFO("Clearing node database - preserving favorites");
|
||||||
for (size_t i = 0; i < meshNodes->size(); i++) {
|
// Compact favorites into contiguous low slots: zeroing in place leaves one above
|
||||||
meshtastic_NodeInfoLite &node = meshNodes->at(i);
|
// numMeshNodes, invisible to every `i < numMeshNodes` scan yet still serialized to flash.
|
||||||
if (i > 0 && !nodeInfoLiteIsFavorite(&node)) {
|
for (size_t i = 1; i < meshNodes->size(); i++) {
|
||||||
eraseNodeSatellites(node.num);
|
const meshtastic_NodeInfoLite &node = meshNodes->at(i);
|
||||||
node = meshtastic_NodeInfoLite();
|
if (nodeInfoLiteIsFavorite(&node)) {
|
||||||
} else {
|
if (numMeshNodes != i)
|
||||||
|
meshNodes->at(numMeshNodes) = node;
|
||||||
numMeshNodes += 1;
|
numMeshNodes += 1;
|
||||||
|
} else if (node.num) {
|
||||||
|
eraseNodeSatellites(node.num);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.end(), meshtastic_NodeInfoLite());
|
||||||
} else {
|
} else {
|
||||||
LOG_INFO("Clearing node database - removing favorites");
|
LOG_INFO("Clearing node database - removing favorites");
|
||||||
for (size_t i = 1; i < meshNodes->size(); i++) {
|
for (size_t i = 1; i < meshNodes->size(); i++) {
|
||||||
@@ -1960,6 +1989,13 @@ void NodeDB::installDefaultDeviceState()
|
|||||||
memcpy(owner.macaddr, ourMacAddr, sizeof(owner.macaddr));
|
memcpy(owner.macaddr, ourMacAddr, sizeof(owner.macaddr));
|
||||||
owner.has_is_unmessagable = true;
|
owner.has_is_unmessagable = true;
|
||||||
owner.is_unmessagable = false;
|
owner.is_unmessagable = false;
|
||||||
|
|
||||||
|
#ifdef HAS_HAM_2M_ONLY
|
||||||
|
// Ham-band-only hardware defaults to licensed operation. The user can still flip this off later
|
||||||
|
// (e.g. a commercial operator on an adjacent allocation who wants to keep encryption on) - we
|
||||||
|
// only set the default here, not on every boot.
|
||||||
|
owner.is_licensed = true;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// We reserve a few nodenums for future use
|
// We reserve a few nodenums for future use
|
||||||
@@ -3218,6 +3254,11 @@ uint32_t sinceLastSeen(const meshtastic_NodeInfoLite *n)
|
|||||||
|
|
||||||
uint32_t sinceReceived(const meshtastic_MeshPacket *p)
|
uint32_t sinceReceived(const meshtastic_MeshPacket *p)
|
||||||
{
|
{
|
||||||
|
// rx_time may be a millis() placeholder while has_rx_time is false - don't age it as
|
||||||
|
// wall-clock, and don't pass it off as "just now" either.
|
||||||
|
if (!p->has_rx_time)
|
||||||
|
return SINCE_UNKNOWN;
|
||||||
|
|
||||||
uint32_t now = getTime();
|
uint32_t now = getTime();
|
||||||
|
|
||||||
int delta = (int)(now - p->rx_time);
|
int delta = (int)(now - p->rx_time);
|
||||||
@@ -3480,10 +3521,9 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact)
|
|||||||
*/
|
*/
|
||||||
bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelIndex, bool xeddsaSigned)
|
bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelIndex, bool xeddsaSigned)
|
||||||
{
|
{
|
||||||
// Only a signed update may change the identity of a node that has proven it signs; our own record is
|
// Only a signed update may change the identity of a proven signer; our own record is exempt.
|
||||||
// exempt. Checked before getOrCreateMeshNode so a refused update cannot evict or write the warm tier.
|
// Checked before getOrCreateMeshNode so a refusal cannot evict; isKnownXeddsaSigner covers the warm tier.
|
||||||
const meshtastic_NodeInfoLite *existing = getMeshNode(nodeId);
|
if (nodeId != getNodeNum() && isKnownXeddsaSigner(nodeId) && !xeddsaSigned) {
|
||||||
if (nodeId != getNodeNum() && existing && nodeInfoLiteHasXeddsaSigned(existing) && !xeddsaSigned) {
|
|
||||||
LOG_WARN("Refusing unsigned identity update for node 0x%08x that previously signed", nodeId);
|
LOG_WARN("Refusing unsigned identity update for node 0x%08x that previously signed", nodeId);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -3614,11 +3654,24 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mp.rx_time) // if the packet has a valid timestamp use it to update our last_heard
|
// Gate on has_rx_time, not truthiness - rx_time may hold a millis() placeholder.
|
||||||
|
if (mp.has_rx_time)
|
||||||
info->last_heard = mp.rx_time;
|
info->last_heard = mp.rx_time;
|
||||||
|
|
||||||
if (mp.rx_snr)
|
// Gate on the packet actually having been received over our own radio, not on rx_snr being
|
||||||
|
// truthy, because 0 dB is valid. TRANSPORT_LORA is set only on the real over-the-air RX path
|
||||||
|
// (RadioInterface.cpp); it excludes TRANSPORT_INTERNAL and TRANSPORT_MQTT, while still accepting
|
||||||
|
// an MQTT-origin packet that a gateway rebroadcasts onto LoRa - we genuinely measured that one
|
||||||
|
// ourselves. Mirrors hop histogram below.
|
||||||
|
// Belt-and-braces: also require has_rx_rssi, which every genuine RF-reception site sets
|
||||||
|
// unconditionally alongside rx_snr - unlike PhoneAPI's replay packets, which set TRANSPORT_LORA
|
||||||
|
// too (so the client treats restored history as if heard over the air) but never has_rx_rssi.
|
||||||
|
// Replay packets don't reach updateFrom() today; this check guards against a future change that
|
||||||
|
// routes them back through this path silently recording a replayed rx_snr as a fresh measurement.
|
||||||
|
if (mp.transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA && mp.has_rx_rssi) {
|
||||||
info->snr = mp.rx_snr; // keep the most recent SNR we received for this node.
|
info->snr = mp.rx_snr; // keep the most recent SNR we received for this node.
|
||||||
|
nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_HAS_SNR_MASK, true);
|
||||||
|
}
|
||||||
|
|
||||||
nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_VIA_MQTT_MASK,
|
nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_VIA_MQTT_MASK,
|
||||||
mp.via_mqtt); // Store if we received this packet via MQTT
|
mp.via_mqtt); // Store if we received this packet via MQTT
|
||||||
@@ -3630,6 +3683,11 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp)
|
|||||||
// inflate the local mesh-size estimate with non-RF nodes (and they usually carry
|
// inflate the local mesh-size estimate with non-RF nodes (and they usually carry
|
||||||
// hop_start==0, landing in the hop-0 bucket that pulls the recommendation lowest), so
|
// hop_start==0, landing in the hop-0 bucket that pulls the recommendation lowest), so
|
||||||
// exclude via_mqtt too.
|
// exclude via_mqtt too.
|
||||||
|
//
|
||||||
|
// The std::max clamp below is deliberate, not a bug: Counting an unproven-but-real neighbor as 0 hops is the
|
||||||
|
// conservative direction. This intentionally does not agree with the `hopsAway >= 0`
|
||||||
|
// gate below, which rejects the same -1 rather than storing it as a fabricated 0 -
|
||||||
|
// the histogram and the stored hops_away serve different purposes.
|
||||||
if (mp.transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA && !mp.via_mqtt &&
|
if (mp.transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA && !mp.via_mqtt &&
|
||||||
hopScalingModule) {
|
hopScalingModule) {
|
||||||
uint8_t hopCount = std::max(int8_t(0), getHopsAway(mp));
|
uint8_t hopCount = std::max(int8_t(0), getHopsAway(mp));
|
||||||
|
|||||||
+18
-2
@@ -166,10 +166,15 @@ inline bool isRadioProfileFile(const char *filename)
|
|||||||
strcmp(filename, backupFileName) == 0;
|
strcmp(filename, backupFileName) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// "No trustworthy arrival time", as distinct from "zero seconds ago". Deliberately huge so the
|
||||||
|
/// display formatters fall into their existing unknown-age branches ("unknown age" / "?").
|
||||||
|
inline constexpr uint32_t SINCE_UNKNOWN = UINT32_MAX;
|
||||||
|
|
||||||
/// Given a node, return how many seconds in the past (vs now) that we last heard from it
|
/// Given a node, return how many seconds in the past (vs now) that we last heard from it
|
||||||
uint32_t sinceLastSeen(const meshtastic_NodeInfoLite *n);
|
uint32_t sinceLastSeen(const meshtastic_NodeInfoLite *n);
|
||||||
|
|
||||||
/// Given a packet, return how many seconds in the past (vs now) it was received
|
/// Given a packet, return how many seconds in the past (vs now) it was received,
|
||||||
|
/// or SINCE_UNKNOWN if it carries no trustworthy rx_time.
|
||||||
uint32_t sinceReceived(const meshtastic_MeshPacket *p);
|
uint32_t sinceReceived(const meshtastic_MeshPacket *p);
|
||||||
|
|
||||||
/// Outcome of mapping a single on-wire last-byte (next_hop / relay_node) back to a full NodeNum.
|
/// Outcome of mapping a single on-wire last-byte (next_hop / relay_node) back to a full NodeNum.
|
||||||
@@ -740,7 +745,12 @@ extern uint32_t error_address;
|
|||||||
#define NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK (1u << NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_SHIFT)
|
#define NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK (1u << NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_SHIFT)
|
||||||
#define NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_SHIFT 9
|
#define NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_SHIFT 9
|
||||||
#define NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_MASK (1u << NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_SHIFT)
|
#define NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_MASK (1u << NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_SHIFT)
|
||||||
// Bits 10..31 reserved for future single-bit flags.
|
// snr_q4 (persisted, sint32) is proto3 singular, so 0 == "never written", but 0 dB is valid.
|
||||||
|
// This bit disambiguates: whenever snr_q4 is written from a genuine RF measurement.
|
||||||
|
// Use this instead of `if (snr_q4)`. Legacy records (bit clear) are unambiguously "unknown".
|
||||||
|
#define NODEINFO_BITFIELD_HAS_SNR_SHIFT 10
|
||||||
|
#define NODEINFO_BITFIELD_HAS_SNR_MASK (1u << NODEINFO_BITFIELD_HAS_SNR_SHIFT)
|
||||||
|
// Bits 11..31 reserved for future single-bit flags.
|
||||||
|
|
||||||
// Convenience accessors so call sites read like the old struct fields.
|
// Convenience accessors so call sites read like the old struct fields.
|
||||||
inline bool nodeInfoLiteHasUser(const meshtastic_NodeInfoLite *n)
|
inline bool nodeInfoLiteHasUser(const meshtastic_NodeInfoLite *n)
|
||||||
@@ -783,6 +793,12 @@ inline bool nodeInfoLiteHasXeddsaSigned(const meshtastic_NodeInfoLite *n)
|
|||||||
{
|
{
|
||||||
return n && (n->bitfield & NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_MASK);
|
return n && (n->bitfield & NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_MASK);
|
||||||
}
|
}
|
||||||
|
/// True if this node's snr_q4 was written from a genuine RF measurement (including a real
|
||||||
|
/// 0 dB reading). False means "never measured" - do not treat 0 as data.
|
||||||
|
inline bool nodeInfoLiteHasSnr(const meshtastic_NodeInfoLite *n)
|
||||||
|
{
|
||||||
|
return n && (n->bitfield & NODEINFO_BITFIELD_HAS_SNR_MASK);
|
||||||
|
}
|
||||||
/// A node that the eviction/migration paths must not drop: a favourite, an
|
/// A node that the eviction/migration paths must not drop: a favourite, an
|
||||||
/// ignored (blocked) node, or a manually-verified key.
|
/// ignored (blocked) node, or a manually-verified key.
|
||||||
inline bool nodeInfoLiteIsProtected(const meshtastic_NodeInfoLite *n)
|
inline bool nodeInfoLiteIsProtected(const meshtastic_NodeInfoLite *n)
|
||||||
|
|||||||
+58
-9
@@ -767,6 +767,12 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
|||||||
LOG_DEBUG("Send config: bluetooth");
|
LOG_DEBUG("Send config: bluetooth");
|
||||||
fromRadioScratch.config.which_payload_variant = meshtastic_Config_bluetooth_tag;
|
fromRadioScratch.config.which_payload_variant = meshtastic_Config_bluetooth_tag;
|
||||||
fromRadioScratch.config.payload_variant.bluetooth = config.bluetooth;
|
fromRadioScratch.config.payload_variant.bluetooth = config.bluetooth;
|
||||||
|
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||||
|
if (!getAdminAuthorized()) {
|
||||||
|
// The pairing PIN is a shared secret; never expose it to an unauthenticated client.
|
||||||
|
fromRadioScratch.config.payload_variant.bluetooth.fixed_pin = 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
break;
|
break;
|
||||||
case meshtastic_Config_security_tag:
|
case meshtastic_Config_security_tag:
|
||||||
LOG_DEBUG("Send config: security");
|
LOG_DEBUG("Send config: security");
|
||||||
@@ -1222,17 +1228,40 @@ uint32_t makeReplayPacketId(NodeNum num, uint32_t timestamp, uint32_t kind)
|
|||||||
return h ? h : 1; // some clients treat id 0 as "unset"
|
return h ? h : 1; // some clients treat id 0 as "unset"
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Populate hop_start/hop_limit from the node's real last-known hop count (if any) so a
|
/// Populate hop_start/hop_limit from the node's last-known hop count - never fabricate one.
|
||||||
/// replayed packet doesn't read as "heard directly" when it wasn't.
|
/// hop_start == 0 with no decoded bitfield means unknown, not a direct neighbor; clients must
|
||||||
|
/// treat it that way too (see hop_start in mesh.proto).
|
||||||
void setReplayHopFields(meshtastic_MeshPacket &pkt, const meshtastic_NodeInfoLite *header)
|
void setReplayHopFields(meshtastic_MeshPacket &pkt, const meshtastic_NodeInfoLite *header)
|
||||||
{
|
{
|
||||||
|
if (!header || !header->has_hops_away) {
|
||||||
|
pkt.hop_start = 0; // unknown - do not fabricate a direct-neighbor reading
|
||||||
|
pkt.hop_limit = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
uint8_t hopLimit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
uint8_t hopLimit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
||||||
uint8_t hopsAway = (header && header->has_hops_away) ? header->hops_away : 0;
|
uint8_t hopsAway = header->hops_away;
|
||||||
pkt.hop_start = hopLimit;
|
pkt.hop_start = hopLimit;
|
||||||
pkt.hop_limit = hopsAway < hopLimit ? (uint8_t)(hopLimit - hopsAway) : 0;
|
pkt.hop_limit = hopsAway < hopLimit ? (uint8_t)(hopLimit - hopsAway) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 2020-01-01: a boot-relative counter needs ~50 years of uptime to reach this, so it cannot be
|
||||||
|
/// confused with a real epoch.
|
||||||
|
constexpr uint32_t MIN_PLAUSIBLE_EPOCH = 1577836800u;
|
||||||
|
|
||||||
|
/// Not every last_heard writer gates on RTC quality - NodeDB::addFromContact stamps it with a bare
|
||||||
|
/// getTime(), which is boot-relative seconds on a node that has never had a clock.
|
||||||
|
bool lastHeardIsWallClock(const meshtastic_NodeInfoLite *header)
|
||||||
|
{
|
||||||
|
return header && header->last_heard >= MIN_PLAUSIBLE_EPOCH;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
// Replayed packets deliberately leave rx_rssi absent. NodeInfoLite stores no RSSI, and
|
||||||
|
// rx_rssi has explicit presence on the wire, indicating "unknown".
|
||||||
|
// Previously these packets carried a bare 0, which a client renders as a real reading.
|
||||||
|
// Note the asymmetry with rx_snr below: that field is still proto3 singular, so "unknown" and
|
||||||
|
// "0 dB" remain indistinguishable there.
|
||||||
meshtastic_MeshPacket PhoneAPI::makeReplayPositionPacket(NodeNum num, const meshtastic_PositionLite &pos)
|
meshtastic_MeshPacket PhoneAPI::makeReplayPositionPacket(NodeNum num, const meshtastic_PositionLite &pos)
|
||||||
{
|
{
|
||||||
// Shape this exactly like a fresh live broadcast Position from the peer so the
|
// Shape this exactly like a fresh live broadcast Position from the peer so the
|
||||||
@@ -1242,12 +1271,18 @@ meshtastic_MeshPacket PhoneAPI::makeReplayPositionPacket(NodeNum num, const mesh
|
|||||||
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
||||||
pkt.from = num;
|
pkt.from = num;
|
||||||
pkt.to = NODENUM_BROADCAST;
|
pkt.to = NODENUM_BROADCAST;
|
||||||
pkt.rx_time = pos.time;
|
// rx_time means "when *we* received this" - use last_heard, not the position's own GPS
|
||||||
|
// fix time (which is often 0 and, when present, already round-trips inside the payload
|
||||||
|
// via ConvertToPosition).
|
||||||
|
pkt.rx_time = header ? header->last_heard : 0;
|
||||||
|
// Present only when last_heard is a genuine epoch - see lastHeardIsWallClock().
|
||||||
|
pkt.has_rx_time = lastHeardIsWallClock(header);
|
||||||
// Stable per-node/per-fix id: replaying the same unchanged history on every
|
// Stable per-node/per-fix id: replaying the same unchanged history on every
|
||||||
// reconnect must not look like a brand new packet to the phone's history/dedup.
|
// reconnect must not look like a brand new packet to the phone's history/dedup.
|
||||||
pkt.id = makeReplayPacketId(num, pkt.rx_time, meshtastic_PortNum_POSITION_APP);
|
pkt.id = makeReplayPacketId(num, pkt.rx_time, meshtastic_PortNum_POSITION_APP);
|
||||||
pkt.channel = 0;
|
pkt.channel = header ? header->channel : 0;
|
||||||
pkt.rx_snr = header ? header->snr : 0;
|
pkt.rx_snr = header ? header->snr : 0;
|
||||||
|
pkt.via_mqtt = nodeInfoLiteViaMqtt(header);
|
||||||
setReplayHopFields(pkt, header);
|
setReplayHopFields(pkt, header);
|
||||||
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||||
// Mark as if heard over the air, not internally generated
|
// Mark as if heard over the air, not internally generated
|
||||||
@@ -1269,9 +1304,12 @@ meshtastic_MeshPacket PhoneAPI::makeReplayTelemetryPacket(NodeNum num, const mes
|
|||||||
// No native timestamp on telemetry packets here; use last_heard.
|
// No native timestamp on telemetry packets here; use last_heard.
|
||||||
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
||||||
pkt.rx_time = header ? header->last_heard : 0;
|
pkt.rx_time = header ? header->last_heard : 0;
|
||||||
|
// Present only when last_heard is a genuine epoch - see lastHeardIsWallClock().
|
||||||
|
pkt.has_rx_time = lastHeardIsWallClock(header);
|
||||||
pkt.id = makeReplayPacketId(num, pkt.rx_time, meshtastic_Telemetry_device_metrics_tag);
|
pkt.id = makeReplayPacketId(num, pkt.rx_time, meshtastic_Telemetry_device_metrics_tag);
|
||||||
pkt.channel = 0;
|
pkt.channel = header ? header->channel : 0;
|
||||||
pkt.rx_snr = header ? header->snr : 0;
|
pkt.rx_snr = header ? header->snr : 0;
|
||||||
|
pkt.via_mqtt = nodeInfoLiteViaMqtt(header);
|
||||||
setReplayHopFields(pkt, header);
|
setReplayHopFields(pkt, header);
|
||||||
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||||
// Mark as if heard over the air, not internally generated - iOS client filters
|
// Mark as if heard over the air, not internally generated - iOS client filters
|
||||||
@@ -1374,9 +1412,12 @@ meshtastic_MeshPacket PhoneAPI::makeReplayEnvironmentPacket(uint32_t num, const
|
|||||||
pkt.to = NODENUM_BROADCAST;
|
pkt.to = NODENUM_BROADCAST;
|
||||||
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
||||||
pkt.rx_time = header ? header->last_heard : 0;
|
pkt.rx_time = header ? header->last_heard : 0;
|
||||||
|
// Present only when last_heard is a genuine epoch - see lastHeardIsWallClock().
|
||||||
|
pkt.has_rx_time = lastHeardIsWallClock(header);
|
||||||
pkt.id = makeReplayPacketId(num, pkt.rx_time, meshtastic_Telemetry_environment_metrics_tag);
|
pkt.id = makeReplayPacketId(num, pkt.rx_time, meshtastic_Telemetry_environment_metrics_tag);
|
||||||
pkt.channel = 0;
|
pkt.channel = header ? header->channel : 0;
|
||||||
pkt.rx_snr = header ? header->snr : 0;
|
pkt.rx_snr = header ? header->snr : 0;
|
||||||
|
pkt.via_mqtt = nodeInfoLiteViaMqtt(header);
|
||||||
setReplayHopFields(pkt, header);
|
setReplayHopFields(pkt, header);
|
||||||
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||||
// Mark as if heard over the air, not internally generated - iOS client filters
|
// Mark as if heard over the air, not internally generated - iOS client filters
|
||||||
@@ -1438,9 +1479,12 @@ meshtastic_MeshPacket PhoneAPI::makeReplayStatusPacket(uint32_t num, const mesht
|
|||||||
// StatusMessage has no native timestamp; use last_heard.
|
// StatusMessage has no native timestamp; use last_heard.
|
||||||
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
||||||
pkt.rx_time = header ? header->last_heard : 0;
|
pkt.rx_time = header ? header->last_heard : 0;
|
||||||
|
// Present only when last_heard is a genuine epoch - see lastHeardIsWallClock().
|
||||||
|
pkt.has_rx_time = lastHeardIsWallClock(header);
|
||||||
pkt.id = makeReplayPacketId(num, pkt.rx_time, meshtastic_PortNum_NODE_STATUS_APP);
|
pkt.id = makeReplayPacketId(num, pkt.rx_time, meshtastic_PortNum_NODE_STATUS_APP);
|
||||||
pkt.channel = 0;
|
pkt.channel = header ? header->channel : 0;
|
||||||
pkt.rx_snr = header ? header->snr : 0;
|
pkt.rx_snr = header ? header->snr : 0;
|
||||||
|
pkt.via_mqtt = nodeInfoLiteViaMqtt(header);
|
||||||
setReplayHopFields(pkt, header);
|
setReplayHopFields(pkt, header);
|
||||||
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||||
// Mark as if heard over the air, not internally generated - client filters
|
// Mark as if heard over the air, not internally generated - client filters
|
||||||
@@ -1817,7 +1861,12 @@ bool PhoneAPI::handleToRadioPacket(meshtastic_MeshPacket &p)
|
|||||||
p.want_ack = true;
|
p.want_ack = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastPortNumToRadio[p.decoded.portnum] = millis();
|
// Only the rate-limited ports above are ever read back, so recording any other portnum would let
|
||||||
|
// a client grow this map without bound by cycling through them.
|
||||||
|
if (IS_ONE_OF(p.decoded.portnum, meshtastic_PortNum_TRACEROUTE_APP, meshtastic_PortNum_POSITION_APP,
|
||||||
|
meshtastic_PortNum_WAYPOINT_APP, meshtastic_PortNum_ALERT_APP, meshtastic_PortNum_TELEMETRY_APP,
|
||||||
|
meshtastic_PortNum_TEXT_MESSAGE_APP))
|
||||||
|
lastPortNumToRadio[p.decoded.portnum] = millis();
|
||||||
service->handleToRadio(p);
|
service->handleToRadio(p);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,6 +113,11 @@ void RF95Interface::setTransmitEnable(bool txon)
|
|||||||
/// \return true if initialisation succeeded.
|
/// \return true if initialisation succeeded.
|
||||||
bool RF95Interface::init()
|
bool RF95Interface::init()
|
||||||
{
|
{
|
||||||
|
#ifdef RF95_POWER_EN
|
||||||
|
pinMode(RF95_POWER_EN, OUTPUT);
|
||||||
|
digitalWrite(RF95_POWER_EN, HIGH);
|
||||||
|
#endif
|
||||||
|
|
||||||
RadioLibInterface::init();
|
RadioLibInterface::init();
|
||||||
|
|
||||||
#if defined(RADIOMASTER_900_BANDIT_NANO) || defined(RADIOMASTER_900_BANDIT)
|
#if defined(RADIOMASTER_900_BANDIT_NANO) || defined(RADIOMASTER_900_BANDIT)
|
||||||
@@ -262,6 +267,7 @@ void RF95Interface::addReceiveMetadata(meshtastic_MeshPacket *mp)
|
|||||||
{
|
{
|
||||||
mp->rx_snr = lora->getSNR();
|
mp->rx_snr = lora->getSNR();
|
||||||
mp->rx_rssi = lround(lora->getRSSI());
|
mp->rx_rssi = lround(lora->getRSSI());
|
||||||
|
mp->has_rx_rssi = true; // rx_rssi has explicit presence - a genuine reading must be marked present to survive encoding
|
||||||
LOG_DEBUG("Corrected frequency offset: %f", lora->getFrequencyError());
|
LOG_DEBUG("Corrected frequency offset: %f", lora->getFrequencyError());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,6 +341,10 @@ bool RF95Interface::sleep()
|
|||||||
setStandby(); // First cancel any active receiving/sending
|
setStandby(); // First cancel any active receiving/sending
|
||||||
lora->sleep();
|
lora->sleep();
|
||||||
|
|
||||||
|
#ifdef RF95_POWER_EN
|
||||||
|
digitalWrite(RF95_POWER_EN, LOW);
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef RF95_FAN_EN
|
#ifdef RF95_FAN_EN
|
||||||
digitalWrite(RF95_FAN_EN, 0);
|
digitalWrite(RF95_FAN_EN, 0);
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -401,6 +401,8 @@ std::unique_ptr<RadioInterface> initLoRa()
|
|||||||
return std::unique_ptr<RadioInterface>(new LR1121Interface(hal, cs, irq, rst, busy));
|
return std::unique_ptr<RadioInterface>(new LR1121Interface(hal, cs, irq, rst, busy));
|
||||||
case use_llcc68:
|
case use_llcc68:
|
||||||
return std::unique_ptr<RadioInterface>(new LLCC68Interface(hal, cs, irq, rst, busy));
|
return std::unique_ptr<RadioInterface>(new LLCC68Interface(hal, cs, irq, rst, busy));
|
||||||
|
case use_lr2021:
|
||||||
|
return std::unique_ptr<RadioInterface>(new LR2021Interface(hal, cs, irq, rst, busy));
|
||||||
case use_simradio:
|
case use_simradio:
|
||||||
return std::unique_ptr<RadioInterface>(new SimRadio);
|
return std::unique_ptr<RadioInterface>(new SimRadio);
|
||||||
default:
|
default:
|
||||||
@@ -877,11 +879,11 @@ void printPacket(const char *prefix, const meshtastic_MeshPacket *p)
|
|||||||
out += DEBUG_PORT.mt_sprintf(" len=%d", p->encrypted.size + sizeof(PacketHeader));
|
out += DEBUG_PORT.mt_sprintf(" len=%d", p->encrypted.size + sizeof(PacketHeader));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (p->rx_time != 0)
|
if (p->has_rx_time) // rx_time has explicit presence; a millis() placeholder isn't a real reading to print
|
||||||
out += DEBUG_PORT.mt_sprintf(" rxtime=%u", p->rx_time);
|
out += DEBUG_PORT.mt_sprintf(" rxtime=%u", p->rx_time);
|
||||||
if (p->rx_snr != 0.0)
|
if (p->rx_snr != 0.0)
|
||||||
out += DEBUG_PORT.mt_sprintf(" rxSNR=%g", p->rx_snr);
|
out += DEBUG_PORT.mt_sprintf(" rxSNR=%g", p->rx_snr);
|
||||||
if (p->rx_rssi != 0)
|
if (p->has_rx_rssi) // rx_rssi has explicit presence; a != 0 check would hide a genuine 0 dBm reading
|
||||||
out += DEBUG_PORT.mt_sprintf(" rxRSSI=%i", p->rx_rssi);
|
out += DEBUG_PORT.mt_sprintf(" rxRSSI=%i", p->rx_rssi);
|
||||||
if (p->via_mqtt != 0)
|
if (p->via_mqtt != 0)
|
||||||
out += DEBUG_PORT.mt_sprintf(" via MQTT");
|
out += DEBUG_PORT.mt_sprintf(" via MQTT");
|
||||||
|
|||||||
@@ -16,6 +16,9 @@
|
|||||||
*/
|
*/
|
||||||
ErrorCode ReliableRouter::send(meshtastic_MeshPacket *p)
|
ErrorCode ReliableRouter::send(meshtastic_MeshPacket *p)
|
||||||
{
|
{
|
||||||
|
const GlobalPacketId key(p);
|
||||||
|
const bool retransmitting = p->want_ack;
|
||||||
|
|
||||||
if (p->want_ack) {
|
if (p->want_ack) {
|
||||||
DEBUG_HEAP_BEFORE;
|
DEBUG_HEAP_BEFORE;
|
||||||
auto copy = packetPool.allocCopy(*p);
|
auto copy = packetPool.allocCopy(*p);
|
||||||
@@ -34,7 +37,12 @@ ErrorCode ReliableRouter::send(meshtastic_MeshPacket *p)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return isBroadcast(p->to) ? FloodingRouter::send(p) : NextHopRouter::send(p);
|
ErrorCode result = isBroadcast(p->to) ? FloodingRouter::send(p) : NextHopRouter::send(p);
|
||||||
|
// Duty-cycle rejections may clear before the scheduled retry.
|
||||||
|
if (retransmitting && result != ERRNO_OK && result != meshtastic_Routing_Error_DUTY_CYCLE_LIMIT)
|
||||||
|
stopRetransmission(key);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ReliableRouter::shouldFilterReceived(const meshtastic_MeshPacket *p)
|
bool ReliableRouter::shouldFilterReceived(const meshtastic_MeshPacket *p)
|
||||||
@@ -196,4 +204,4 @@ bool ReliableRouter::shouldSuccessAckWithWantAck(const meshtastic_MeshPacket *p)
|
|||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
+92
-38
@@ -5,6 +5,7 @@
|
|||||||
#include "MeshService.h"
|
#include "MeshService.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "PositionPrecision.h"
|
#include "PositionPrecision.h"
|
||||||
|
#include "UptimeClock.h"
|
||||||
#include "gps/RTC.h"
|
#include "gps/RTC.h"
|
||||||
|
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
@@ -269,6 +270,19 @@ PacketId generatePacketId()
|
|||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RxTimeStamp computeRxTimeStamp()
|
||||||
|
{
|
||||||
|
const bool haveTime = getRTCQuality() >= RTCQualityFromNet;
|
||||||
|
return {haveTime ? getValidTime(RTCQualityFromNet) : Time::getMillis(), haveTime};
|
||||||
|
}
|
||||||
|
|
||||||
|
void stampRxTime(meshtastic_MeshPacket *p)
|
||||||
|
{
|
||||||
|
const RxTimeStamp ts = computeRxTimeStamp();
|
||||||
|
p->rx_time = ts.time;
|
||||||
|
p->has_rx_time = ts.valid;
|
||||||
|
}
|
||||||
|
|
||||||
meshtastic_MeshPacket *Router::allocForSending()
|
meshtastic_MeshPacket *Router::allocForSending()
|
||||||
{
|
{
|
||||||
meshtastic_MeshPacket *p = packetPool.allocZeroed();
|
meshtastic_MeshPacket *p = packetPool.allocZeroed();
|
||||||
@@ -280,8 +294,8 @@ meshtastic_MeshPacket *Router::allocForSending()
|
|||||||
p->to = NODENUM_BROADCAST;
|
p->to = NODENUM_BROADCAST;
|
||||||
p->hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
p->hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
||||||
p->id = generatePacketId();
|
p->id = generatePacketId();
|
||||||
p->rx_time =
|
// Just in case we process the packet locally - make sure it has a timestamp.
|
||||||
getValidTime(RTCQualityFromNet); // Just in case we process the packet locally - make sure it has a valid timestamp
|
stampRxTime(p);
|
||||||
|
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
@@ -451,6 +465,8 @@ ErrorCode Router::send(meshtastic_MeshPacket *p)
|
|||||||
|
|
||||||
if (!(p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag ||
|
if (!(p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag ||
|
||||||
p->which_payload_variant == meshtastic_MeshPacket_decoded_tag)) {
|
p->which_payload_variant == meshtastic_MeshPacket_decoded_tag)) {
|
||||||
|
// Error returns from here own the packet, as the position-precision path below does.
|
||||||
|
packetPool.release(p);
|
||||||
return meshtastic_Routing_Error_BAD_REQUEST;
|
return meshtastic_Routing_Error_BAD_REQUEST;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -619,7 +635,9 @@ bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p)
|
|||||||
if (p->decoded.xeddsa_signature.size == XEDDSA_SIGNATURE_SIZE) {
|
if (p->decoded.xeddsa_signature.size == XEDDSA_SIGNATURE_SIZE) {
|
||||||
meshtastic_NodeInfoLite_public_key_t senderKey = {0, {0}};
|
meshtastic_NodeInfoLite_public_key_t senderKey = {0, {0}};
|
||||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(p->from);
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(p->from);
|
||||||
if (nodeDB->copyPublicKey(p->from, senderKey)) {
|
// Authoritative keys only: verifying against an opportunistic cache key would let a planted
|
||||||
|
// key mark its own node a signer, the trust loop #11116 closed on the decrypt path.
|
||||||
|
if (nodeDB->copyPublicKeyAuthoritative(p->from, senderKey)) {
|
||||||
p->xeddsa_signed =
|
p->xeddsa_signed =
|
||||||
crypto->xeddsa_verify(senderKey.bytes, p->from, p->id, p->decoded.portnum, p->decoded.payload.bytes,
|
crypto->xeddsa_verify(senderKey.bytes, p->from, p->id, p->decoded.portnum, p->decoded.payload.bytes,
|
||||||
p->decoded.payload.size, p->decoded.xeddsa_signature.bytes);
|
p->decoded.payload.size, p->decoded.xeddsa_signature.bytes);
|
||||||
@@ -667,20 +685,14 @@ bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p)
|
|||||||
if (compatible)
|
if (compatible)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
// In Balanced, preserve legacy unsigned-unicast compatibility and only reject the class a
|
// Balanced rejects only what a signer always signs: non-PKI broadcasts whose signed encoding
|
||||||
// signing node always signs: a non-PKI broadcast whose signed encoding would still fit the
|
// would have fit, plus unicasts on ham where licensed senders sign too. Mirrors perhapsEncode.
|
||||||
// LoRa frame. Canonical sizing removes unknown protobuf fields before mirroring the
|
if (nodeDB->isKnownXeddsaSigner(p->from) && (isBroadcast(p->to) || owner.is_licensed)) {
|
||||||
// sender-side signedDataFits() gate, so this counts the same fields that gate counted.
|
|
||||||
// Unicast packets and broadcasts too big to carry a signature are never signed, so they
|
|
||||||
// must not be hard-failed here even for a known signer (PKI already returned above).
|
|
||||||
// isKnownXeddsaSigner consults the warm tier too: a signer evicted from the hot store
|
|
||||||
// must not become impersonatable via unsigned broadcasts until it is re-heard.
|
|
||||||
if (nodeDB->isKnownXeddsaSigner(p->from) && isBroadcast(p->to)) {
|
|
||||||
size_t canonicalSize;
|
size_t canonicalSize;
|
||||||
if (!canonicalSignableSize(&p->decoded, &canonicalSize))
|
if (!canonicalSignableSize(&p->decoded, &canonicalSize))
|
||||||
return true; // can't size it; never drop on a sizing failure
|
return true; // can't size it; never drop on a sizing failure
|
||||||
if (canonicalSize + XEDDSA_SIGNATURE_FIELD_BYTES + MESHTASTIC_HEADER_LENGTH <= MAX_LORA_PAYLOAD_LEN) {
|
if (canonicalSize + XEDDSA_SIGNATURE_FIELD_BYTES + MESHTASTIC_HEADER_LENGTH <= MAX_LORA_PAYLOAD_LEN) {
|
||||||
LOG_WARN("Dropping unsigned broadcast from 0x%08x that previously signed", p->from);
|
LOG_WARN("Dropping unsigned packet from 0x%08x that previously signed", p->from);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1158,7 +1170,12 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
|
|||||||
*destKey.bytes);
|
*destKey.bytes);
|
||||||
return meshtastic_Routing_Error_PKI_FAILED;
|
return meshtastic_Routing_Error_PKI_FAILED;
|
||||||
}
|
}
|
||||||
crypto->encryptCurve25519(p->to, getFrom(p), destKey, p->id, numbytes, bytes, p->encrypted.bytes);
|
// On failure encrypted.bytes holds no ciphertext, so continuing would put the plaintext
|
||||||
|
// on the air labelled pki_encrypted.
|
||||||
|
if (!crypto->encryptCurve25519(p->to, getFrom(p), destKey, p->id, numbytes, bytes, p->encrypted.bytes)) {
|
||||||
|
LOG_WARN("PKI encryption failed for destination node 0x%08x", p->to);
|
||||||
|
return meshtastic_Routing_Error_PKI_FAILED;
|
||||||
|
}
|
||||||
numbytes += MESHTASTIC_PKC_OVERHEAD;
|
numbytes += MESHTASTIC_PKC_OVERHEAD;
|
||||||
p->channel = 0;
|
p->channel = 0;
|
||||||
p->pki_encrypted = true;
|
p->pki_encrypted = true;
|
||||||
@@ -1232,7 +1249,12 @@ bool Router::dequeueDeferredLocal(DeferredLocal &out)
|
|||||||
void Router::deliverLocal(meshtastic_MeshPacket *p, RxSource src)
|
void Router::deliverLocal(meshtastic_MeshPacket *p, RxSource src)
|
||||||
{
|
{
|
||||||
// Top level: handle synchronously, exactly as before the depth guard existed.
|
// Top level: handle synchronously, exactly as before the depth guard existed.
|
||||||
if (handleDepth == 0) {
|
bool nested;
|
||||||
|
{
|
||||||
|
concurrency::LockGuard g(&deferredLock);
|
||||||
|
nested = handleDepth > 0;
|
||||||
|
}
|
||||||
|
if (!nested) {
|
||||||
handleReceived(p, src);
|
handleReceived(p, src);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1241,8 +1263,26 @@ void Router::deliverLocal(meshtastic_MeshPacket *p, RxSource src)
|
|||||||
// handleReceived() drains it once the current dispatch unwinds, instead of stacking another
|
// handleReceived() drains it once the current dispatch unwinds, instead of stacking another
|
||||||
// handleReceived() frame on top of the module handler (nRF52 stack overflow on config save).
|
// handleReceived() frame on top of the module handler (nRF52 stack overflow on config save).
|
||||||
meshtastic_MeshPacket *copy = packetPool.allocCopy(*p);
|
meshtastic_MeshPacket *copy = packetPool.allocCopy(*p);
|
||||||
if (copy && enqueueDeferredLocal(copy, src))
|
if (copy) {
|
||||||
return;
|
// Re-check depth under the lock that also gates the drain's decrement, so a drain finishing
|
||||||
|
// while we allocated cannot leave this copy stranded in the ring.
|
||||||
|
bool stillNested = false, queued = false;
|
||||||
|
{
|
||||||
|
concurrency::LockGuard g(&deferredLock);
|
||||||
|
stillNested = handleDepth > 0;
|
||||||
|
if (stillNested)
|
||||||
|
queued = enqueueDeferredLocal(copy, src);
|
||||||
|
}
|
||||||
|
if (queued)
|
||||||
|
return;
|
||||||
|
if (!stillNested) {
|
||||||
|
// The drain finished first, so nothing would pick this up. Go through handleReceived()
|
||||||
|
// rather than dispatchReceived() so a loopback from its modules still defers.
|
||||||
|
handleReceived(copy, src);
|
||||||
|
packetPool.release(copy);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Pool exhausted or queue full: drop the deferral. Leak-free and degraded but safe - the
|
// Pool exhausted or queue full: drop the deferral. Leak-free and degraded but safe - the
|
||||||
// packet still followed its normal non-loopback path (SHOULD_RELEASE, or the TX path for a
|
// packet still followed its normal non-loopback path (SHOULD_RELEASE, or the TX path for a
|
||||||
@@ -1261,29 +1301,39 @@ void Router::deliverLocal(meshtastic_MeshPacket *p, RxSource src)
|
|||||||
*/
|
*/
|
||||||
void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
|
void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
|
||||||
{
|
{
|
||||||
handleDepth++;
|
{
|
||||||
|
concurrency::LockGuard g(&deferredLock);
|
||||||
|
handleDepth++;
|
||||||
#ifdef PIO_UNIT_TESTING
|
#ifdef PIO_UNIT_TESTING
|
||||||
if (handleDepth > maxHandleDepthObserved)
|
if (handleDepth > maxHandleDepthObserved)
|
||||||
maxHandleDepthObserved = handleDepth;
|
maxHandleDepthObserved = handleDepth;
|
||||||
#endif
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
dispatchReceived(p, src);
|
dispatchReceived(p, src);
|
||||||
|
|
||||||
// Only the outermost frame drains. Deferred packets were produced by modules sending from
|
// Decide "am I the last frame" and drop the depth in one critical section. Splitting them lets
|
||||||
// inside dispatchReceived()'s callModules(); process them here, after the triggering frame has
|
// two frames both read the same pre-decrement value, skip the drain, and strand the ring.
|
||||||
// unwound, so a second handleReceived() never sits on top of a module handler. handleDepth
|
for (;;) {
|
||||||
// stays >= 1 through the drain, so a drained packet whose own modules send more loopback
|
|
||||||
// packets enqueues them for this same loop rather than recursing: the stack stays flat and
|
|
||||||
// processing is breadth-first.
|
|
||||||
if (handleDepth == 1) {
|
|
||||||
DeferredLocal d;
|
DeferredLocal d;
|
||||||
while (dequeueDeferredLocal(d)) {
|
{
|
||||||
dispatchReceived(d.p, d.src);
|
concurrency::LockGuard g(&deferredLock);
|
||||||
packetPool.release(d.p);
|
if (handleDepth > 1) {
|
||||||
|
// Another frame is still live and will own the drain once it is last.
|
||||||
|
handleDepth--;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!dequeueDeferredLocal(d)) {
|
||||||
|
// Last frame and nothing queued, so zero is reached only with the ring empty.
|
||||||
|
handleDepth--;
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Depth stays at 1 across the drain, so a loopback from these modules defers instead of
|
||||||
|
// recursing, and dispatch runs outside the lock.
|
||||||
|
dispatchReceived(d.p, d.src);
|
||||||
|
packetPool.release(d.p);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDepth--;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Router::dispatchReceived(meshtastic_MeshPacket *p, RxSource src)
|
void Router::dispatchReceived(meshtastic_MeshPacket *p, RxSource src)
|
||||||
@@ -1305,12 +1355,15 @@ void Router::dispatchReceived(meshtastic_MeshPacket *p, RxSource src)
|
|||||||
if (src == RX_SRC_RADIO)
|
if (src == RX_SRC_RADIO)
|
||||||
applyRoutingAuthCache(p);
|
applyRoutingAuthCache(p);
|
||||||
|
|
||||||
// Also, we should set the time from the ISR and it should have msec level resolution.
|
|
||||||
// Keep the decoded working packet and encrypted MQTT copy on the same local arrival timestamp.
|
// Keep the decoded working packet and encrypted MQTT copy on the same local arrival timestamp.
|
||||||
const uint32_t rxTime = getValidTime(RTCQualityFromNet);
|
// See computeRxTimeStamp() for the placeholder/has_rx_time semantics.
|
||||||
p->rx_time = rxTime;
|
const RxTimeStamp rxStamp = computeRxTimeStamp();
|
||||||
if (p_encrypted)
|
p->rx_time = rxStamp.time;
|
||||||
p_encrypted->rx_time = rxTime;
|
p->has_rx_time = rxStamp.valid;
|
||||||
|
if (p_encrypted) {
|
||||||
|
p_encrypted->rx_time = rxStamp.time;
|
||||||
|
p_encrypted->has_rx_time = rxStamp.valid;
|
||||||
|
}
|
||||||
|
|
||||||
// Take those raw bytes and convert them back into a well structured protobuf we can understand
|
// Take those raw bytes and convert them back into a well structured protobuf we can understand
|
||||||
auto decodedState = perhapsDecode(p);
|
auto decodedState = perhapsDecode(p);
|
||||||
@@ -1443,7 +1496,8 @@ void Router::perhapsHandleReceived(meshtastic_MeshPacket *p)
|
|||||||
#if ARCH_PORTDUINO
|
#if ARCH_PORTDUINO
|
||||||
// Even ignored packets get logged in the trace
|
// Even ignored packets get logged in the trace
|
||||||
if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) {
|
if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) {
|
||||||
p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone
|
// Store the arrival timestamp for the phone before it's traced.
|
||||||
|
stampRxTime(p);
|
||||||
LOG_TRACE("%s", MeshPacketSerializer::JsonSerializeEncrypted(p).c_str());
|
LOG_TRACE("%s", MeshPacketSerializer::JsonSerializeEncrypted(p).c_str());
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+20
-6
@@ -7,9 +7,21 @@
|
|||||||
#include "PacketHistory.h"
|
#include "PacketHistory.h"
|
||||||
#include "PointerQueue.h"
|
#include "PointerQueue.h"
|
||||||
#include "RadioInterface.h"
|
#include "RadioInterface.h"
|
||||||
|
#include "concurrency/LockGuard.h"
|
||||||
#include "concurrency/OSThread.h"
|
#include "concurrency/OSThread.h"
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
|
/// rx_time/has_rx_time for "now": a real epoch when the clock is trustworthy, else a
|
||||||
|
/// Time::getMillis() placeholder with valid=false.
|
||||||
|
struct RxTimeStamp {
|
||||||
|
uint32_t time;
|
||||||
|
bool valid;
|
||||||
|
};
|
||||||
|
RxTimeStamp computeRxTimeStamp();
|
||||||
|
|
||||||
|
/// Stamp p->rx_time/p->has_rx_time with computeRxTimeStamp().
|
||||||
|
void stampRxTime(meshtastic_MeshPacket *p);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A mesh aware router that supports multiple interfaces.
|
* A mesh aware router that supports multiple interfaces.
|
||||||
*/
|
*/
|
||||||
@@ -151,12 +163,8 @@ class Router : protected concurrency::OSThread, protected PacketHistory
|
|||||||
void perhapsHandleReceived(meshtastic_MeshPacket *p);
|
void perhapsHandleReceived(meshtastic_MeshPacket *p);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called from perhapsHandleReceived() - allows subclass message delivery behavior.
|
* Called from perhapsHandleReceived() for radio ingress and from deliverLocal() for our own
|
||||||
* Handle any packet that is received by an interface on this node.
|
* loopback, so p may be locally generated. Does NOT free p; the caller still owns it.
|
||||||
* Note: some packets may merely being passed through this node and will be forwarded elsewhere.
|
|
||||||
*
|
|
||||||
* Note: this packet will never be called for messages sent/generated by this node.
|
|
||||||
* Note: this method will free the provided packet.
|
|
||||||
*/
|
*/
|
||||||
void handleReceived(meshtastic_MeshPacket *p, RxSource src = RX_SRC_RADIO);
|
void handleReceived(meshtastic_MeshPacket *p, RxSource src = RX_SRC_RADIO);
|
||||||
|
|
||||||
@@ -181,6 +189,10 @@ class Router : protected concurrency::OSThread, protected PacketHistory
|
|||||||
/// so a locally-sent loopback packet must be deferred rather than handled synchronously.
|
/// so a locally-sent loopback packet must be deferred rather than handled synchronously.
|
||||||
uint8_t handleDepth = 0;
|
uint8_t handleDepth = 0;
|
||||||
|
|
||||||
|
/// Guards handleDepth and the deferred ring below. nRF52 drives the router from the BLE task as
|
||||||
|
/// well as the loop task, so both are read-modify-written from two tasks.
|
||||||
|
concurrency::Lock deferredLock;
|
||||||
|
|
||||||
/// A local loopback packet whose handleReceived() was deferred because it was produced from
|
/// A local loopback packet whose handleReceived() was deferred because it was produced from
|
||||||
/// inside callModules(). The queue owns the packet; its RxSource travels with it so the drain
|
/// inside callModules(). The queue owns the packet; its RxSource travels with it so the drain
|
||||||
/// dispatches it with the origin the sender intended (RX_SRC_LOCAL stays local).
|
/// dispatches it with the origin the sender intended (RX_SRC_LOCAL stays local).
|
||||||
@@ -199,8 +211,10 @@ class Router : protected concurrency::OSThread, protected PacketHistory
|
|||||||
uint8_t deferredLocalCount = 0; // entries currently queued
|
uint8_t deferredLocalCount = 0; // entries currently queued
|
||||||
|
|
||||||
/// Queue a deferred local packet. Returns false (and queues nothing) when full.
|
/// Queue a deferred local packet. Returns false (and queues nothing) when full.
|
||||||
|
/// Caller must hold deferredLock: the enqueue decision is atomic with the drain's depth update.
|
||||||
bool enqueueDeferredLocal(meshtastic_MeshPacket *p, RxSource src);
|
bool enqueueDeferredLocal(meshtastic_MeshPacket *p, RxSource src);
|
||||||
/// Pop the oldest deferred local packet into out. Returns false when empty.
|
/// Pop the oldest deferred local packet into out. Returns false when empty.
|
||||||
|
/// Caller must hold deferredLock.
|
||||||
bool dequeueDeferredLocal(DeferredLocal &out);
|
bool dequeueDeferredLocal(DeferredLocal &out);
|
||||||
|
|
||||||
/** Frees the provided packet, and generates a NAK indicating the specifed error while sending */
|
/** Frees the provided packet, and generates a NAK indicating the specifed error while sending */
|
||||||
|
|||||||
@@ -335,6 +335,7 @@ template <typename T> void SX126xInterface<T>::addReceiveMetadata(meshtastic_Mes
|
|||||||
// LOG_DEBUG("PacketStatus %x", lora.getPacketStatus());
|
// LOG_DEBUG("PacketStatus %x", lora.getPacketStatus());
|
||||||
mp->rx_snr = lora.getSNR();
|
mp->rx_snr = lora.getSNR();
|
||||||
mp->rx_rssi = lround(lora.getRSSI());
|
mp->rx_rssi = lround(lora.getRSSI());
|
||||||
|
mp->has_rx_rssi = true; // rx_rssi has explicit presence - a genuine reading must be marked present to survive encoding
|
||||||
LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError());
|
LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -205,6 +205,7 @@ template <typename T> void SX128xInterface<T>::addReceiveMetadata(meshtastic_Mes
|
|||||||
// LOG_DEBUG("PacketStatus %x", lora.getPacketStatus());
|
// LOG_DEBUG("PacketStatus %x", lora.getPacketStatus());
|
||||||
mp->rx_snr = lora.getSNR();
|
mp->rx_snr = lora.getSNR();
|
||||||
mp->rx_rssi = lround(lora.getRSSI());
|
mp->rx_rssi = lround(lora.getRSSI());
|
||||||
|
mp->has_rx_rssi = true; // rx_rssi has explicit presence - a genuine reading must be marked present to survive encoding
|
||||||
LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError());
|
LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfo
|
|||||||
meshtastic_NodeInfo info = meshtastic_NodeInfo_init_default;
|
meshtastic_NodeInfo info = meshtastic_NodeInfo_init_default;
|
||||||
|
|
||||||
info.num = lite->num;
|
info.num = lite->num;
|
||||||
|
// NodeInfo.snr (wire) is still proto3 singular float - unlike NodeInfoLite.snr_q4, it has no
|
||||||
|
// presence bit and cannot distinguish a genuine 0 dB from "unknown". nodeInfoLiteHasSnr(lite)
|
||||||
|
// is available if a future NodeInfo revision needs to carry that distinction to clients.
|
||||||
info.snr = lite->snr;
|
info.snr = lite->snr;
|
||||||
info.last_heard = lite->last_heard;
|
info.last_heard = lite->last_heard;
|
||||||
info.channel = lite->channel;
|
info.channel = lite->channel;
|
||||||
|
|||||||
@@ -6,6 +6,12 @@
|
|||||||
#include "RadioInterface.h"
|
#include "RadioInterface.h"
|
||||||
#include "modules/NodeInfoModule.h"
|
#include "modules/NodeInfoModule.h"
|
||||||
|
|
||||||
|
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||||
|
// receivePacket() dispatches ToRadio straight to MeshService, bypassing handleToRadioPacket and so
|
||||||
|
// the lockdown admin gate. Fail the build rather than silently ship an admin-auth bypass.
|
||||||
|
#error "USE_PACKET_API is incompatible with MESHTASTIC_PHONEAPI_ACCESS_CONTROL (PacketAPI bypasses the lockdown admin gate)"
|
||||||
|
#endif
|
||||||
|
|
||||||
PacketAPI *packetAPI = nullptr;
|
PacketAPI *packetAPI = nullptr;
|
||||||
|
|
||||||
PacketAPI *PacketAPI::create(PacketServer *_server)
|
PacketAPI *PacketAPI::create(PacketServer *_server)
|
||||||
|
|||||||
@@ -95,8 +95,11 @@ typedef struct _meshtastic_NodeInfoLite {
|
|||||||
/* The public key of the user's device, for PKI-based encrypted DMs. */
|
/* The public key of the user's device, for PKI-based encrypted DMs. */
|
||||||
meshtastic_NodeInfoLite_public_key_t public_key;
|
meshtastic_NodeInfoLite_public_key_t public_key;
|
||||||
/* Q4-encoded SNR: dB × 4, sint32 zigzag. Matches RouteDiscovery convention.
|
/* Q4-encoded SNR: dB × 4, sint32 zigzag. Matches RouteDiscovery convention.
|
||||||
Encode: snr_q4 = (int32_t)(snr * 4.0f). Decode: snr = snr_q4 / 4.0f.
|
Encode: snr_q4 = (int32_t)lroundf(snr * 4.0f). Decode: snr = snr_q4 / 4.0f.
|
||||||
float snr is always zeroed on disk; this field carries all persisted SNR. */
|
float snr is always zeroed on disk; this field carries all persisted SNR.
|
||||||
|
A stored 0 does not by itself mean "unknown" here - see NODEINFO_BITFIELD_HAS_SNR in
|
||||||
|
src/mesh/NodeDB.h for the presence bit that disambiguates a genuine 0 dB reading from
|
||||||
|
"never measured". */
|
||||||
int32_t snr_q4;
|
int32_t snr_q4;
|
||||||
} meshtastic_NodeInfoLite;
|
} meshtastic_NodeInfoLite;
|
||||||
|
|
||||||
|
|||||||
@@ -1079,7 +1079,14 @@ typedef struct _meshtastic_MeshPacket {
|
|||||||
/* The time this message was received by the esp32 (secs since 1970).
|
/* The time this message was received by the esp32 (secs since 1970).
|
||||||
Note: this field is _never_ sent on the radio link itself (to save space) Times
|
Note: this field is _never_ sent on the radio link itself (to save space) Times
|
||||||
are typically not sent over the mesh, but they will be added to any Packet
|
are typically not sent over the mesh, but they will be added to any Packet
|
||||||
(chain of SubPacket) sent to the phone (so the phone can know exact time of reception) */
|
(chain of SubPacket) sent to the phone (so the phone can know exact time of reception)
|
||||||
|
Explicit presence: firmware cannot always attach a trustworthy wall-clock timestamp at the
|
||||||
|
moment of reception - a node with no GPS and no phone connected yet has no time source at
|
||||||
|
all. has_rx_time disambiguates that state from a genuine (if coincidental) 1970-01-01
|
||||||
|
reading. A packet delivered with this field absent may still be re-timestamped once a valid
|
||||||
|
clock becomes available, before the phone ever sees it - "absent" is not guaranteed
|
||||||
|
permanent, only "not yet known at last observation". */
|
||||||
|
bool has_rx_time;
|
||||||
uint32_t rx_time;
|
uint32_t rx_time;
|
||||||
/* *Never* sent over the radio links.
|
/* *Never* sent over the radio links.
|
||||||
Set during reception to indicate the SNR of this packet.
|
Set during reception to indicate the SNR of this packet.
|
||||||
@@ -1103,14 +1110,24 @@ typedef struct _meshtastic_MeshPacket {
|
|||||||
/* The priority of this message for sending.
|
/* The priority of this message for sending.
|
||||||
See MeshPacket.Priority description for more details. */
|
See MeshPacket.Priority description for more details. */
|
||||||
meshtastic_MeshPacket_Priority priority;
|
meshtastic_MeshPacket_Priority priority;
|
||||||
/* rssi of received packet. Only sent to phone for dispay purposes. */
|
/* rssi of received packet. Only sent to phone for dispay purposes.
|
||||||
|
Explicit presence: rssi 0 is a legitimate reading on some radios (SX126x can report exactly
|
||||||
|
0 dBm; SX127x's formula can even go positive), so implicit-presence proto3 made an unset
|
||||||
|
value indistinguishable from a measured one. has_rx_rssi disambiguates; a replayed packet
|
||||||
|
built from history the device never restored an RSSI for should leave this field absent
|
||||||
|
rather than emitting 0. */
|
||||||
|
bool has_rx_rssi;
|
||||||
int32_t rx_rssi;
|
int32_t rx_rssi;
|
||||||
/* Describe if this message is delayed */
|
/* Describe if this message is delayed */
|
||||||
meshtastic_MeshPacket_Delayed delayed;
|
meshtastic_MeshPacket_Delayed delayed;
|
||||||
/* Describes whether this packet passed via MQTT somewhere along the path it currently took. */
|
/* Describes whether this packet passed via MQTT somewhere along the path it currently took. */
|
||||||
bool via_mqtt;
|
bool via_mqtt;
|
||||||
/* Hop limit with which the original packet started. Sent via LoRa using three bits in the unencrypted header.
|
/* Hop limit with which the original packet started. Sent via LoRa using three bits in the unencrypted header.
|
||||||
When receiving a packet, the difference between hop_start and hop_limit gives how many hops it traveled. */
|
When receiving a packet, the difference between hop_start and hop_limit gives how many hops it traveled.
|
||||||
|
hop_start == 0 does not necessarily mean a direct (0-hop) neighbor: firmware prior to 2.3.0
|
||||||
|
never populated this field, so a receiver can only trust hop_start == 0 as genuine once it has
|
||||||
|
decoded the packet and confirmed the sender's bitfield is present (added in 2.5.0). Until then,
|
||||||
|
or for a sender that never sets that bitfield, treat hop_start == 0 as unknown, not direct. */
|
||||||
uint8_t hop_start;
|
uint8_t hop_start;
|
||||||
/* Records the public key the packet was encrypted with, if applicable. */
|
/* Records the public key the packet was encrypted with, if applicable. */
|
||||||
meshtastic_MeshPacket_public_key_t public_key;
|
meshtastic_MeshPacket_public_key_t public_key;
|
||||||
@@ -1730,7 +1747,7 @@ extern "C" {
|
|||||||
#define meshtastic_Waypoint_init_default {0, false, 0, false, 0, 0, 0, "", "", 0, 0, false, meshtastic_BoundingBox_init_default, 0, 0, 0}
|
#define meshtastic_Waypoint_init_default {0, false, 0, false, 0, 0, 0, "", "", 0, 0, false, meshtastic_BoundingBox_init_default, 0, 0, 0}
|
||||||
#define meshtastic_StatusMessage_init_default {""}
|
#define meshtastic_StatusMessage_init_default {""}
|
||||||
#define meshtastic_MqttClientProxyMessage_init_default {"", 0, {{0, {0}}}, 0}
|
#define meshtastic_MqttClientProxyMessage_init_default {"", 0, {{0, {0}}}, 0}
|
||||||
#define meshtastic_MeshPacket_init_default {0, 0, 0, 0, {meshtastic_Data_init_default}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0, _meshtastic_MeshPacket_TransportMechanism_MIN, 0}
|
#define meshtastic_MeshPacket_init_default {0, 0, 0, 0, {meshtastic_Data_init_default}, 0, false, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, false, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0, _meshtastic_MeshPacket_TransportMechanism_MIN, 0}
|
||||||
#define meshtastic_NodeInfo_init_default {0, false, meshtastic_User_init_default, false, meshtastic_Position_init_default, 0, 0, false, meshtastic_DeviceMetrics_init_default, 0, 0, false, 0, 0, 0, 0, 0, 0}
|
#define meshtastic_NodeInfo_init_default {0, false, meshtastic_User_init_default, false, meshtastic_Position_init_default, 0, 0, false, meshtastic_DeviceMetrics_init_default, 0, 0, false, 0, 0, 0, 0, 0, 0}
|
||||||
#define meshtastic_MyNodeInfo_init_default {0, 0, 0, {0, {0}}, "", _meshtastic_FirmwareEdition_MIN, 0}
|
#define meshtastic_MyNodeInfo_init_default {0, 0, 0, {0, {0}}, "", _meshtastic_FirmwareEdition_MIN, 0}
|
||||||
#define meshtastic_LogRecord_init_default {"", 0, "", _meshtastic_LogRecord_Level_MIN}
|
#define meshtastic_LogRecord_init_default {"", 0, "", _meshtastic_LogRecord_Level_MIN}
|
||||||
@@ -1769,7 +1786,7 @@ extern "C" {
|
|||||||
#define meshtastic_Waypoint_init_zero {0, false, 0, false, 0, 0, 0, "", "", 0, 0, false, meshtastic_BoundingBox_init_zero, 0, 0, 0}
|
#define meshtastic_Waypoint_init_zero {0, false, 0, false, 0, 0, 0, "", "", 0, 0, false, meshtastic_BoundingBox_init_zero, 0, 0, 0}
|
||||||
#define meshtastic_StatusMessage_init_zero {""}
|
#define meshtastic_StatusMessage_init_zero {""}
|
||||||
#define meshtastic_MqttClientProxyMessage_init_zero {"", 0, {{0, {0}}}, 0}
|
#define meshtastic_MqttClientProxyMessage_init_zero {"", 0, {{0, {0}}}, 0}
|
||||||
#define meshtastic_MeshPacket_init_zero {0, 0, 0, 0, {meshtastic_Data_init_zero}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0, _meshtastic_MeshPacket_TransportMechanism_MIN, 0}
|
#define meshtastic_MeshPacket_init_zero {0, 0, 0, 0, {meshtastic_Data_init_zero}, 0, false, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, false, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0, _meshtastic_MeshPacket_TransportMechanism_MIN, 0}
|
||||||
#define meshtastic_NodeInfo_init_zero {0, false, meshtastic_User_init_zero, false, meshtastic_Position_init_zero, 0, 0, false, meshtastic_DeviceMetrics_init_zero, 0, 0, false, 0, 0, 0, 0, 0, 0}
|
#define meshtastic_NodeInfo_init_zero {0, false, meshtastic_User_init_zero, false, meshtastic_Position_init_zero, 0, 0, false, meshtastic_DeviceMetrics_init_zero, 0, 0, false, 0, 0, 0, 0, 0, 0}
|
||||||
#define meshtastic_MyNodeInfo_init_zero {0, 0, 0, {0, {0}}, "", _meshtastic_FirmwareEdition_MIN, 0}
|
#define meshtastic_MyNodeInfo_init_zero {0, 0, 0, {0, {0}}, "", _meshtastic_FirmwareEdition_MIN, 0}
|
||||||
#define meshtastic_LogRecord_init_zero {"", 0, "", _meshtastic_LogRecord_Level_MIN}
|
#define meshtastic_LogRecord_init_zero {"", 0, "", _meshtastic_LogRecord_Level_MIN}
|
||||||
@@ -2189,12 +2206,12 @@ X(a, STATIC, SINGULAR, UINT32, channel, 3) \
|
|||||||
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,decoded,decoded), 4) \
|
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,decoded,decoded), 4) \
|
||||||
X(a, STATIC, ONEOF, BYTES, (payload_variant,encrypted,encrypted), 5) \
|
X(a, STATIC, ONEOF, BYTES, (payload_variant,encrypted,encrypted), 5) \
|
||||||
X(a, STATIC, SINGULAR, FIXED32, id, 6) \
|
X(a, STATIC, SINGULAR, FIXED32, id, 6) \
|
||||||
X(a, STATIC, SINGULAR, FIXED32, rx_time, 7) \
|
X(a, STATIC, OPTIONAL, FIXED32, rx_time, 7) \
|
||||||
X(a, STATIC, SINGULAR, FLOAT, rx_snr, 8) \
|
X(a, STATIC, SINGULAR, FLOAT, rx_snr, 8) \
|
||||||
X(a, STATIC, SINGULAR, UINT32, hop_limit, 9) \
|
X(a, STATIC, SINGULAR, UINT32, hop_limit, 9) \
|
||||||
X(a, STATIC, SINGULAR, BOOL, want_ack, 10) \
|
X(a, STATIC, SINGULAR, BOOL, want_ack, 10) \
|
||||||
X(a, STATIC, SINGULAR, UENUM, priority, 11) \
|
X(a, STATIC, SINGULAR, UENUM, priority, 11) \
|
||||||
X(a, STATIC, SINGULAR, INT32, rx_rssi, 12) \
|
X(a, STATIC, OPTIONAL, INT32, rx_rssi, 12) \
|
||||||
X(a, STATIC, SINGULAR, UENUM, delayed, 13) \
|
X(a, STATIC, SINGULAR, UENUM, delayed, 13) \
|
||||||
X(a, STATIC, SINGULAR, BOOL, via_mqtt, 14) \
|
X(a, STATIC, SINGULAR, BOOL, via_mqtt, 14) \
|
||||||
X(a, STATIC, SINGULAR, UINT32, hop_start, 15) \
|
X(a, STATIC, SINGULAR, UINT32, hop_start, 15) \
|
||||||
|
|||||||
@@ -79,6 +79,11 @@ class UdpMulticastHandler final
|
|||||||
LOG_WARN("UDP packet with spoofed local from=0x%08x, dropping", mp.from);
|
LOG_WARN("UDP packet with spoofed local from=0x%08x, dropping", mp.from);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Same clamp the MQTT ingress applies: an out-of-range hop count is not relayable.
|
||||||
|
if (mp.hop_limit > HOP_MAX || mp.hop_start > HOP_MAX) {
|
||||||
|
LOG_WARN("UDP packet with invalid hop_limit(%u) or hop_start(%u), dropping", mp.hop_limit, mp.hop_start);
|
||||||
|
return;
|
||||||
|
}
|
||||||
mp.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP;
|
mp.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP;
|
||||||
// Authentication metadata is local-only; Router re-establishes it after successful PKI decryption.
|
// Authentication metadata is local-only; Router re-establishes it after successful PKI decryption.
|
||||||
mp.pki_encrypted = false;
|
mp.pki_encrypted = false;
|
||||||
@@ -86,9 +91,13 @@ class UdpMulticastHandler final
|
|||||||
UniquePacketPoolPacket p = packetPool.allocUniqueCopy(mp);
|
UniquePacketPoolPacket p = packetPool.allocUniqueCopy(mp);
|
||||||
if (!p)
|
if (!p)
|
||||||
return;
|
return;
|
||||||
// Unset received SNR/RSSI
|
// Unset received SNR/RSSI - no local RF measurement exists for a UDP arrival. rx_rssi
|
||||||
|
// has explicit presence, so also clear has_rx_rssi: `mp` may have arrived already
|
||||||
|
// carrying a real measurement from whichever node forwarded it onto UDP, and leaving
|
||||||
|
// the presence bit set would misrepresent that stale value as "0 dBm over UDP".
|
||||||
p->rx_snr = 0;
|
p->rx_snr = 0;
|
||||||
p->rx_rssi = 0;
|
p->rx_rssi = 0;
|
||||||
|
p->has_rx_rssi = false;
|
||||||
router->enqueueReceivedMessage(p.release());
|
router->enqueueReceivedMessage(p.release());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,6 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
AdminModule *adminModule;
|
AdminModule *adminModule;
|
||||||
bool hasOpenEditTransaction;
|
|
||||||
|
|
||||||
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI)
|
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI)
|
||||||
static bool licensedIdentityWillMigrate()
|
static bool licensedIdentityWillMigrate()
|
||||||
@@ -240,6 +239,9 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
|||||||
return handled;
|
return handled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Before the switch, so every case below sees consistent transaction state.
|
||||||
|
expireStaleEditTransaction();
|
||||||
|
|
||||||
switch (r->which_payload_variant) {
|
switch (r->which_payload_variant) {
|
||||||
|
|
||||||
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
|
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
|
||||||
@@ -481,12 +483,14 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
|||||||
case meshtastic_AdminMessage_begin_edit_settings_tag: {
|
case meshtastic_AdminMessage_begin_edit_settings_tag: {
|
||||||
LOG_INFO("Begin transaction for editing settings");
|
LOG_INFO("Begin transaction for editing settings");
|
||||||
hasOpenEditTransaction = true;
|
hasOpenEditTransaction = true;
|
||||||
|
editTransactionActivityMs = millis();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case meshtastic_AdminMessage_commit_edit_settings_tag: {
|
case meshtastic_AdminMessage_commit_edit_settings_tag: {
|
||||||
disableBluetooth();
|
disableBluetooth();
|
||||||
LOG_INFO("Commit transaction for edited settings");
|
LOG_INFO("Commit transaction for edited settings");
|
||||||
hasOpenEditTransaction = false;
|
hasOpenEditTransaction = false;
|
||||||
|
deferredEditSegments = 0;
|
||||||
saveChanges(SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS | SEGMENT_NODEDATABASE);
|
saveChanges(SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS | SEGMENT_NODEDATABASE);
|
||||||
flushChannelWarnings(); // one coalesced message for everything edited in this transaction
|
flushChannelWarnings(); // one coalesced message for everything edited in this transaction
|
||||||
break;
|
break;
|
||||||
@@ -1875,6 +1879,23 @@ void AdminModule::reboot(int32_t seconds)
|
|||||||
rebootAtMsec = (seconds < 0) ? 0 : (millis() + seconds * 1000);
|
rebootAtMsec = (seconds < 0) ? 0 : (millis() + seconds * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Without this, a commit that never arrives leaves the transaction open forever and every later
|
||||||
|
// config write from any client is applied, acknowledged, and then never saved.
|
||||||
|
void AdminModule::expireStaleEditTransaction()
|
||||||
|
{
|
||||||
|
if (!hasOpenEditTransaction || Throttle::isWithinTimespanMs(editTransactionActivityMs, EDIT_TRANSACTION_IDLE_MS))
|
||||||
|
return;
|
||||||
|
|
||||||
|
LOG_WARN("Edit transaction abandoned for %us; committing what it applied", EDIT_TRANSACTION_IDLE_MS / 1000);
|
||||||
|
hasOpenEditTransaction = false;
|
||||||
|
int segments = deferredEditSegments;
|
||||||
|
deferredEditSegments = 0;
|
||||||
|
// No reboot: the settings are already live in RAM and the client that would expect one is gone.
|
||||||
|
if (segments)
|
||||||
|
saveChanges(segments, false);
|
||||||
|
flushChannelWarnings();
|
||||||
|
}
|
||||||
|
|
||||||
void AdminModule::saveChanges(int saveWhat, bool shouldReboot)
|
void AdminModule::saveChanges(int saveWhat, bool shouldReboot)
|
||||||
{
|
{
|
||||||
#ifdef PIO_UNIT_TESTING
|
#ifdef PIO_UNIT_TESTING
|
||||||
@@ -1885,6 +1906,8 @@ void AdminModule::saveChanges(int saveWhat, bool shouldReboot)
|
|||||||
service->reloadConfig(saveWhat); // Calls saveToDisk among other things
|
service->reloadConfig(saveWhat); // Calls saveToDisk among other things
|
||||||
} else {
|
} else {
|
||||||
LOG_INFO("Delay save of changes to disk until the open transaction is committed");
|
LOG_INFO("Delay save of changes to disk until the open transaction is committed");
|
||||||
|
editTransactionActivityMs = millis(); // still in use, so not the abandoned kind we time out
|
||||||
|
deferredEditSegments |= saveWhat;
|
||||||
}
|
}
|
||||||
if (shouldReboot && !hasOpenEditTransaction) {
|
if (shouldReboot && !hasOpenEditTransaction) {
|
||||||
reboot(DEFAULT_REBOOT_SECONDS);
|
reboot(DEFAULT_REBOOT_SECONDS);
|
||||||
|
|||||||
@@ -40,6 +40,13 @@ class AdminModule : public ProtobufModule<meshtastic_AdminMessage>, public Obser
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
bool hasOpenEditTransaction = false;
|
bool hasOpenEditTransaction = false;
|
||||||
|
// Each deferred write restarts the clock, so this bounds the gap between writes, not the length
|
||||||
|
// of the edit; a bulk import sends them milliseconds apart.
|
||||||
|
static constexpr uint32_t EDIT_TRANSACTION_IDLE_MS = 60 * 1000;
|
||||||
|
uint32_t editTransactionActivityMs = 0; // millis() of the last save this transaction deferred
|
||||||
|
int deferredEditSegments = 0; // segments that transaction has touched but not yet saved
|
||||||
|
/// Retire an open edit transaction whose client stopped talking, persisting what it applied.
|
||||||
|
void expireStaleEditTransaction();
|
||||||
#ifdef PIO_UNIT_TESTING
|
#ifdef PIO_UNIT_TESTING
|
||||||
int lastSaveWhatForTest = 0;
|
int lastSaveWhatForTest = 0;
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -880,8 +880,10 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event)
|
|||||||
// All hardware keys fall through to here (CardKB, physical, etc.)
|
// All hardware keys fall through to here (CardKB, physical, etc.)
|
||||||
|
|
||||||
if (event->kbchar == INPUT_BROKER_MSG_EMOTE_LIST) {
|
if (event->kbchar == INPUT_BROKER_MSG_EMOTE_LIST) {
|
||||||
updateState(CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER);
|
if (graphics::numEmotes > 0) { // no picker on EXCLUDE_EMOJI builds (empty emotes[])
|
||||||
screen->forceDisplay();
|
updateState(CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER);
|
||||||
|
screen->forceDisplay();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Confirm select (Enter)
|
// Confirm select (Enter)
|
||||||
@@ -965,6 +967,11 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event)
|
|||||||
int CannedMessageModule::handleEmotePickerInput(const InputEvent *event)
|
int CannedMessageModule::handleEmotePickerInput(const InputEvent *event)
|
||||||
{
|
{
|
||||||
int numEmotes = graphics::numEmotes;
|
int numEmotes = graphics::numEmotes;
|
||||||
|
if (numEmotes == 0) { // EXCLUDE_EMOJI: emotes[] is empty, any index would read out of bounds
|
||||||
|
updateState(CANNED_MESSAGE_RUN_STATE_FREETEXT, true);
|
||||||
|
screen->forceDisplay();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Override isDown and isSelect ONLY for emote picker behavior
|
// Override isDown and isSelect ONLY for emote picker behavior
|
||||||
bool isUp = isUpEvent(event);
|
bool isUp = isUpEvent(event);
|
||||||
|
|||||||
@@ -32,14 +32,17 @@ ProcessMessage DropzoneModule::handleReceived(const meshtastic_MeshPacket &mp)
|
|||||||
auto &p = mp.decoded;
|
auto &p = mp.decoded;
|
||||||
char matchCompare[54];
|
char matchCompare[54];
|
||||||
auto incomingMessage = reinterpret_cast<const char *>(p.payload.bytes);
|
auto incomingMessage = reinterpret_cast<const char *>(p.payload.bytes);
|
||||||
sprintf(matchCompare, "%s conditions", owner.short_name);
|
// payload.bytes is not NUL-terminated, so a comparison longer than the received size would read
|
||||||
if (strncasecmp(incomingMessage, matchCompare, strlen(matchCompare)) == 0) {
|
// whatever the previous occupant of the packet left behind.
|
||||||
|
const size_t received = p.payload.size;
|
||||||
|
snprintf(matchCompare, sizeof(matchCompare), "%s conditions", owner.short_name);
|
||||||
|
if (received >= strlen(matchCompare) && strncasecmp(incomingMessage, matchCompare, strlen(matchCompare)) == 0) {
|
||||||
LOG_DEBUG("Received dropzone conditions request");
|
LOG_DEBUG("Received dropzone conditions request");
|
||||||
startSendConditions = millis();
|
startSendConditions = millis();
|
||||||
}
|
}
|
||||||
|
|
||||||
sprintf(matchCompare, "%s conditions", owner.long_name);
|
snprintf(matchCompare, sizeof(matchCompare), "%s conditions", owner.long_name);
|
||||||
if (strncasecmp(incomingMessage, matchCompare, strlen(matchCompare)) == 0) {
|
if (received >= strlen(matchCompare) && strncasecmp(incomingMessage, matchCompare, strlen(matchCompare)) == 0) {
|
||||||
LOG_DEBUG("Received dropzone conditions request");
|
LOG_DEBUG("Received dropzone conditions request");
|
||||||
startSendConditions = millis();
|
startSendConditions = millis();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ extern AmbientLightingThread *ambientLightingThread;
|
|||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !defined(CONFIG_IDF_TARGET_ESP32C6)
|
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL)
|
||||||
#include <NonBlockingRtttl.h>
|
#include <NonBlockingRtttl.h>
|
||||||
#else
|
#else
|
||||||
// Noop class for portduino.
|
// Noop class for portduino.
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ void MeshBeaconBroadcastModule::sendBeacon()
|
|||||||
p->hop_limit = 0; // all beacon packets are zero hopped to limit spamming.
|
p->hop_limit = 0; // all beacon packets are zero hopped to limit spamming.
|
||||||
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||||
p->want_ack = false;
|
p->want_ack = false;
|
||||||
p->rx_time = getValidTime(RTCQualityFromNet);
|
stampRxTime(p);
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Packet type decisions ────────────────────────────────────────────────
|
// ── Packet type decisions ────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "NodeStatus.h"
|
#include "NodeStatus.h"
|
||||||
#include "Router.h"
|
#include "Router.h"
|
||||||
#include "TransmitHistory.h"
|
#include "TransmitHistory.h"
|
||||||
|
#include "UptimeClock.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#include "gps/RTC.h"
|
#include "gps/RTC.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
@@ -33,7 +34,9 @@ bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mes
|
|||||||
// Suppress replies to senders we've replied to recently (12H window)
|
// Suppress replies to senders we've replied to recently (12H window)
|
||||||
if (mp.decoded.want_response && !isFromUs(&mp)) {
|
if (mp.decoded.want_response && !isFromUs(&mp)) {
|
||||||
const NodeNum sender = getFrom(&mp);
|
const NodeNum sender = getFrom(&mp);
|
||||||
const uint32_t now = mp.rx_time ? mp.rx_time : getTime();
|
// A local dedup window, not a wall-clock reading - uptime avoids RTC-quality jumps and
|
||||||
|
// replayed packets' stale rx_time perturbing it.
|
||||||
|
const uint32_t now = (uint32_t)(Time::getMillis64() / 1000);
|
||||||
auto it = lastNodeInfoSeen.find(sender);
|
auto it = lastNodeInfoSeen.find(sender);
|
||||||
if (it != lastNodeInfoSeen.end()) {
|
if (it != lastNodeInfoSeen.end()) {
|
||||||
uint32_t sinceLast = now >= it->second ? now - it->second : 0;
|
uint32_t sinceLast = now >= it->second ? now - it->second : 0;
|
||||||
@@ -50,10 +53,9 @@ bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mes
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
NodeNum sourceNum = getFrom(&mp);
|
NodeNum sourceNum = getFrom(&mp);
|
||||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(sourceNum);
|
// Broadcasts only: unicast NodeInfo is unsigned off ham, so updateUser refuses the identity
|
||||||
// Broadcasts only: senders never sign unicast NodeInfo, so dropping it would break exchanges
|
// write instead. isKnownXeddsaSigner also covers the warm tier.
|
||||||
// with signer nodes. Backstops ingress that skips Router's downgrade drop (e.g. decoded MQTT).
|
if (nodeDB->isKnownXeddsaSigner(sourceNum) && !mp.xeddsa_signed && isBroadcast(mp.to)) {
|
||||||
if (node && nodeInfoLiteHasXeddsaSigned(node) && !mp.xeddsa_signed && isBroadcast(mp.to)) {
|
|
||||||
LOG_WARN("Dropping unsigned NodeInfo broadcast from node 0x%08x that previously signed", sourceNum);
|
LOG_WARN("Dropping unsigned NodeInfo broadcast from node 0x%08x that previously signed", sourceNum);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,9 +167,9 @@ bool PositionModule::hasGPS()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Allocate a packet with our position data if we have one
|
// Allocate a packet with our position data if we have one
|
||||||
meshtastic_MeshPacket *PositionModule::allocPositionPacket()
|
meshtastic_MeshPacket *PositionModule::allocPositionPacket(uint32_t atPrecision)
|
||||||
{
|
{
|
||||||
if (precision == 0) {
|
if (atPrecision == 0) {
|
||||||
LOG_DEBUG("Skip location send because precision is set to 0!");
|
LOG_DEBUG("Skip location send because precision is set to 0!");
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -196,12 +196,12 @@ meshtastic_MeshPacket *PositionModule::allocPositionPacket()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// lat/lon are unconditionally included - IF AVAILABLE!
|
// lat/lon are unconditionally included - IF AVAILABLE!
|
||||||
LOG_DEBUG("Send location with precision %i", precision);
|
LOG_DEBUG("Send location with precision %i", atPrecision);
|
||||||
p.latitude_i = localPosition.latitude_i;
|
p.latitude_i = localPosition.latitude_i;
|
||||||
p.longitude_i = localPosition.longitude_i;
|
p.longitude_i = localPosition.longitude_i;
|
||||||
p.has_latitude_i = true;
|
p.has_latitude_i = true;
|
||||||
p.has_longitude_i = true;
|
p.has_longitude_i = true;
|
||||||
applyPositionPrecision(p, precision);
|
applyPositionPrecision(p, atPrecision);
|
||||||
// Always use NTP / GPS time if available
|
// Always use NTP / GPS time if available
|
||||||
if (getValidTime(RTCQualityNTP) > 0) {
|
if (getValidTime(RTCQualityNTP) > 0) {
|
||||||
p.time = getValidTime(RTCQualityNTP);
|
p.time = getValidTime(RTCQualityNTP);
|
||||||
@@ -281,7 +281,7 @@ meshtastic_MeshPacket *PositionModule::allocReply()
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
meshtastic_MeshPacket *reply = allocPositionPacket();
|
meshtastic_MeshPacket *reply = allocPositionPacket(precision);
|
||||||
if (reply) {
|
if (reply) {
|
||||||
lastSentReply = millis(); // Track when we sent this reply
|
lastSentReply = millis(); // Track when we sent this reply
|
||||||
}
|
}
|
||||||
@@ -373,13 +373,34 @@ void PositionModule::sendOurPosition()
|
|||||||
currentGeneration = radioGeneration;
|
currentGeneration = radioGeneration;
|
||||||
|
|
||||||
// If we changed channels, ask everyone else for their latest info
|
// If we changed channels, ask everyone else for their latest info
|
||||||
LOG_INFO("Send pos@%x:6 to mesh (wantReplies=%d)", localPosition.timestamp, requestReplies);
|
|
||||||
for (uint8_t channelNum = 0; channelNum < 8; channelNum++) {
|
for (uint8_t channelNum = 0; channelNum < 8; channelNum++) {
|
||||||
if (getPositionPrecisionForChannel(channelNum) != 0) {
|
if (getPositionPrecisionForChannel(channelNum) != 0) {
|
||||||
|
LOG_INFO("Send pos@%x:6 to mesh (wantReplies=%d)", localPosition.timestamp, requestReplies);
|
||||||
sendOurPosition(NODENUM_BROADCAST, requestReplies, channelNum);
|
sendOurPosition(NODENUM_BROADCAST, requestReplies, channelNum);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
LOG_INFO("Skip pos@%x:6 broadcast; position sharing is opt-in and disabled on all channels", localPosition.timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position broadcasts are opt-in per channel in 2.8, but our own position still plays to the
|
||||||
|
// connected phone/UI at full precision, like telemetry does. This copy never touches the mesh.
|
||||||
|
// Returns true only when a packet was actually handed to the phone queue, so the caller can
|
||||||
|
// hold off the cadence stamp (and retry soon) after a guard or allocation failure.
|
||||||
|
bool PositionModule::sendOurPositionToPhone()
|
||||||
|
{
|
||||||
|
if (!config.position.fixed_position && !nodeDB->hasLocalPositionSinceBoot())
|
||||||
|
return false; // Same stale-restored-position guard as sendOurPosition()
|
||||||
|
|
||||||
|
meshtastic_MeshPacket *p = allocPositionPacket(32);
|
||||||
|
if (p == nullptr)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
p->to = NODENUM_BROADCAST;
|
||||||
|
p->decoded.want_response = false;
|
||||||
|
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||||
|
service->sendToPhone(p);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PositionModule::sendOurPosition(NodeNum dest, bool wantReplies, uint8_t channel)
|
void PositionModule::sendOurPosition(NodeNum dest, bool wantReplies, uint8_t channel)
|
||||||
@@ -396,7 +417,7 @@ void PositionModule::sendOurPosition(NodeNum dest, bool wantReplies, uint8_t cha
|
|||||||
// Set the class precision value for this particular packet.
|
// Set the class precision value for this particular packet.
|
||||||
precision = getPositionPrecisionForChannel(channel);
|
precision = getPositionPrecisionForChannel(channel);
|
||||||
|
|
||||||
meshtastic_MeshPacket *p = allocPositionPacket();
|
meshtastic_MeshPacket *p = allocPositionPacket(precision);
|
||||||
if (p == nullptr) {
|
if (p == nullptr) {
|
||||||
LOG_DEBUG("allocPositionPacket returned a nullptr");
|
LOG_DEBUG("allocPositionPacket returned a nullptr");
|
||||||
return;
|
return;
|
||||||
@@ -465,6 +486,14 @@ bool PositionModule::positionWithinPrecisionCell(int32_t aLat, int32_t aLon, int
|
|||||||
truncateCoordinate(aLon, precision) == truncateCoordinate(bLon, precision);
|
truncateCoordinate(aLon, precision) == truncateCoordinate(bLon, precision);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool PositionModule::shouldSendPositionToPhone(bool hasValidPosition, bool phoneQueueEmpty, bool everSentToPhone, uint32_t nowMs,
|
||||||
|
uint32_t lastSentMs, uint32_t intervalMs)
|
||||||
|
{
|
||||||
|
if (!hasValidPosition || !phoneQueueEmpty)
|
||||||
|
return false;
|
||||||
|
return !everSentToPhone || (nowMs - lastSentMs) >= intervalMs;
|
||||||
|
}
|
||||||
|
|
||||||
uint32_t PositionModule::effectiveBroadcastIntervalMs(uint32_t configuredIntervalMs, bool stationary, uint32_t stationaryFloorMs)
|
uint32_t PositionModule::effectiveBroadcastIntervalMs(uint32_t configuredIntervalMs, bool stationary, uint32_t stationaryFloorMs)
|
||||||
{
|
{
|
||||||
if (stationary && stationaryFloorMs > configuredIntervalMs)
|
if (stationary && stationaryFloorMs > configuredIntervalMs)
|
||||||
@@ -485,8 +514,20 @@ int32_t PositionModule::runOnce()
|
|||||||
if (node == nullptr)
|
if (node == nullptr)
|
||||||
return RUNONCE_INTERVAL;
|
return RUNONCE_INTERVAL;
|
||||||
|
|
||||||
// We limit our GPS broadcasts to a max rate
|
|
||||||
uint32_t now = millis();
|
uint32_t now = millis();
|
||||||
|
|
||||||
|
// Local-only delivery, so it runs regardless of mesh opt-in state or channel utilization.
|
||||||
|
// Only send while the queue is empty (phone assumed connected), like telemetry. The cadence
|
||||||
|
// stamp only advances when a packet was actually queued, so a guard or allocation failure
|
||||||
|
// retries on the next tick instead of waiting out a full interval.
|
||||||
|
if (shouldSendPositionToPhone(nodeDB->hasValidPosition(node), service->isToPhoneQueueEmpty(), hasSentPositionToPhone, now,
|
||||||
|
lastPhoneSendMs, sendToPhoneIntervalMs) &&
|
||||||
|
sendOurPositionToPhone()) {
|
||||||
|
hasSentPositionToPhone = true;
|
||||||
|
lastPhoneSendMs = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We limit our GPS broadcasts to a max rate
|
||||||
uint32_t intervalMs = Default::getConfiguredOrDefaultMsScaled(
|
uint32_t intervalMs = Default::getConfiguredOrDefaultMsScaled(
|
||||||
config.position.position_broadcast_secs, default_broadcast_interval_secs, numOnlineNodes, TrafficType::POSITION);
|
config.position.position_broadcast_secs, default_broadcast_interval_secs, numOnlineNodes, TrafficType::POSITION);
|
||||||
uint32_t msSinceLastSend = now - lastGpsSend;
|
uint32_t msSinceLastSend = now - lastGpsSend;
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ class PositionModule : public ProtobufModule<meshtastic_Position>, private concu
|
|||||||
// Effective min interval: stationary positions are held to stationaryFloorMs (when that is the
|
// Effective min interval: stationary positions are held to stationaryFloorMs (when that is the
|
||||||
// longer of the two); otherwise the normal configured interval.
|
// longer of the two); otherwise the normal configured interval.
|
||||||
static uint32_t effectiveBroadcastIntervalMs(uint32_t configuredIntervalMs, bool stationary, uint32_t stationaryFloorMs);
|
static uint32_t effectiveBroadcastIntervalMs(uint32_t configuredIntervalMs, bool stationary, uint32_t stationaryFloorMs);
|
||||||
|
// Pure local-play policy: stream our own position to the phone/UI when we have a valid
|
||||||
|
// position, the phone queue is idle, and the cadence has elapsed. everSentToPhone (rather
|
||||||
|
// than a lastSentMs sentinel) marks the never-sent state, so a send stamped at millis()==0
|
||||||
|
// still honors the cadence.
|
||||||
|
static bool shouldSendPositionToPhone(bool hasValidPosition, bool phoneQueueEmpty, bool everSentToPhone, uint32_t nowMs,
|
||||||
|
uint32_t lastSentMs, uint32_t intervalMs);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
/** Called to handle a particular incoming message
|
/** Called to handle a particular incoming message
|
||||||
@@ -63,7 +69,14 @@ class PositionModule : public ProtobufModule<meshtastic_Position>, private concu
|
|||||||
virtual int32_t runOnce() override;
|
virtual int32_t runOnce() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
meshtastic_MeshPacket *allocPositionPacket();
|
meshtastic_MeshPacket *allocPositionPacket(uint32_t atPrecision);
|
||||||
|
// Streams our own position to the connected phone/UI at full precision without touching the
|
||||||
|
// mesh. Keeps the local view alive now that mesh position sharing is opt-in. Returns true
|
||||||
|
// only when a packet was handed to the phone queue.
|
||||||
|
bool sendOurPositionToPhone();
|
||||||
|
bool hasSentPositionToPhone = false;
|
||||||
|
uint32_t lastPhoneSendMs = 0;
|
||||||
|
static constexpr uint32_t sendToPhoneIntervalMs = 60 * 1000; // Matches telemetry's local cadence
|
||||||
struct SmartPosition getDistanceTraveledSinceLastSend(meshtastic_PositionLite currentPosition);
|
struct SmartPosition getDistanceTraveledSinceLastSend(meshtastic_PositionLite currentPosition);
|
||||||
// True when our position is unchanged since the last broadcast: it truncates to the same
|
// True when our position is unchanged since the last broadcast: it truncates to the same
|
||||||
// precision grid cell, so re-sending would be a duplicate that traffic management dedups
|
// precision grid cell, so re-sending would be a duplicate that traffic management dedups
|
||||||
|
|||||||
@@ -103,8 +103,10 @@ bool RemoteHardwareModule::handleReceivedProtobuf(const meshtastic_MeshPacket &r
|
|||||||
r.gpio_value = res;
|
r.gpio_value = res;
|
||||||
r.gpio_mask = p.gpio_mask;
|
r.gpio_mask = p.gpio_mask;
|
||||||
meshtastic_MeshPacket *p2 = allocDataProtobuf(r);
|
meshtastic_MeshPacket *p2 = allocDataProtobuf(r);
|
||||||
setReplyTo(p2, req);
|
if (p2) {
|
||||||
myReply = p2;
|
setReplyTo(p2, req);
|
||||||
|
myReply = p2;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +151,8 @@ int32_t RemoteHardwareModule::runOnce()
|
|||||||
r.type = meshtastic_HardwareMessage_Type_GPIOS_CHANGED;
|
r.type = meshtastic_HardwareMessage_Type_GPIOS_CHANGED;
|
||||||
r.gpio_value = curVal;
|
r.gpio_value = curVal;
|
||||||
meshtastic_MeshPacket *p = allocDataProtobuf(r);
|
meshtastic_MeshPacket *p = allocDataProtobuf(r);
|
||||||
service->sendToMesh(p);
|
if (p)
|
||||||
|
service->sendToMesh(p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -64,19 +64,20 @@ void RoutingModule::sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketI
|
|||||||
uint8_t RoutingModule::getHopLimitForResponse(const meshtastic_MeshPacket &mp)
|
uint8_t RoutingModule::getHopLimitForResponse(const meshtastic_MeshPacket &mp)
|
||||||
{
|
{
|
||||||
const int8_t hopsUsed = getHopsAway(mp);
|
const int8_t hopsUsed = getHopsAway(mp);
|
||||||
|
const uint8_t responseHopLimit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
||||||
if (hopsUsed >= 0) {
|
if (hopsUsed >= 0) {
|
||||||
if (hopsUsed > (int32_t)(config.lora.hop_limit)) {
|
if (hopsUsed > static_cast<int32_t>(responseHopLimit)) {
|
||||||
// In event mode, we never want to send packets with more than our default 3 hops.
|
// In event mode, never exceed the configured event hop limit.
|
||||||
#if !(EVENTMODE) // This falls through to the default.
|
#if !USERPREFS_EVENT_MODE // This falls through to the default.
|
||||||
return hopsUsed; // If the request used more hops than the limit, use the same amount of hops
|
return hopsUsed; // If the request used more hops than the limit, use the same amount of hops
|
||||||
#endif
|
#endif
|
||||||
} else if (mp.hop_start == 0) {
|
} else if (mp.hop_start == 0) {
|
||||||
return 0; // The requesting node wanted 0 hops, so the response also uses a direct/local path.
|
return 0; // The requesting node wanted 0 hops, so the response also uses a direct/local path.
|
||||||
} else if ((uint8_t)(hopsUsed + 2) < config.lora.hop_limit) {
|
} else if (static_cast<uint8_t>(hopsUsed + 2) < responseHopLimit) {
|
||||||
return hopsUsed + 2; // Use only the amount of hops needed with some margin as the way back may be different
|
return hopsUsed + 2; // Use only the amount of hops needed with some margin as the way back may be different
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); // Use the default hop limit
|
return responseHopLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
meshtastic_MeshPacket *RoutingModule::allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex,
|
meshtastic_MeshPacket *RoutingModule::allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex,
|
||||||
@@ -93,4 +94,4 @@ RoutingModule::RoutingModule() : ProtobufModule("routing", meshtastic_PortNum_RO
|
|||||||
// LocalOnly requires either the from or to to be a known node
|
// LocalOnly requires either the from or to to be a known node
|
||||||
// knownOnly specifically requires the from to be a known node.
|
// knownOnly specifically requires the from to be a known node.
|
||||||
encryptedOk = true;
|
encryptedOk = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,6 +193,9 @@ void StoreForwardModule::historyAdd(const meshtastic_MeshPacket &mp)
|
|||||||
}
|
}
|
||||||
|
|
||||||
this->packetHistory[this->packetHistoryTotalCount].time = getTime();
|
this->packetHistory[this->packetHistoryTotalCount].time = getTime();
|
||||||
|
// getTime() silently falls back to a boot-relative count with no RTC source at all; record
|
||||||
|
// whether it was actually trustworthy so replay doesn't have to guess from current quality.
|
||||||
|
this->packetHistory[this->packetHistoryTotalCount].has_rx_time = (getRTCQuality() >= RTCQualityFromNet);
|
||||||
this->packetHistory[this->packetHistoryTotalCount].to = mp.to;
|
this->packetHistory[this->packetHistoryTotalCount].to = mp.to;
|
||||||
this->packetHistory[this->packetHistoryTotalCount].channel = mp.channel;
|
this->packetHistory[this->packetHistoryTotalCount].channel = mp.channel;
|
||||||
this->packetHistory[this->packetHistoryTotalCount].from = getFrom(&mp);
|
this->packetHistory[this->packetHistoryTotalCount].from = getFrom(&mp);
|
||||||
@@ -201,6 +204,7 @@ void StoreForwardModule::historyAdd(const meshtastic_MeshPacket &mp)
|
|||||||
this->packetHistory[this->packetHistoryTotalCount].emoji = (bool)p.emoji;
|
this->packetHistory[this->packetHistoryTotalCount].emoji = (bool)p.emoji;
|
||||||
this->packetHistory[this->packetHistoryTotalCount].payload_size = p.payload.size;
|
this->packetHistory[this->packetHistoryTotalCount].payload_size = p.payload.size;
|
||||||
this->packetHistory[this->packetHistoryTotalCount].rx_rssi = mp.rx_rssi;
|
this->packetHistory[this->packetHistoryTotalCount].rx_rssi = mp.rx_rssi;
|
||||||
|
this->packetHistory[this->packetHistoryTotalCount].has_rx_rssi = mp.has_rx_rssi;
|
||||||
this->packetHistory[this->packetHistoryTotalCount].rx_snr = mp.rx_snr;
|
this->packetHistory[this->packetHistoryTotalCount].rx_snr = mp.rx_snr;
|
||||||
this->packetHistory[this->packetHistoryTotalCount].hop_start = mp.hop_start;
|
this->packetHistory[this->packetHistoryTotalCount].hop_start = mp.hop_start;
|
||||||
this->packetHistory[this->packetHistoryTotalCount].hop_limit = mp.hop_limit;
|
this->packetHistory[this->packetHistoryTotalCount].hop_limit = mp.hop_limit;
|
||||||
@@ -257,8 +261,10 @@ meshtastic_MeshPacket *StoreForwardModule::preparePayload(NodeNum dest, uint32_t
|
|||||||
p->channel = this->packetHistory[i].channel;
|
p->channel = this->packetHistory[i].channel;
|
||||||
p->decoded.reply_id = this->packetHistory[i].reply_id;
|
p->decoded.reply_id = this->packetHistory[i].reply_id;
|
||||||
p->rx_time = this->packetHistory[i].time;
|
p->rx_time = this->packetHistory[i].time;
|
||||||
|
p->has_rx_time = this->packetHistory[i].has_rx_time; // presence captured at store time, not replay time
|
||||||
p->decoded.emoji = (uint32_t)this->packetHistory[i].emoji;
|
p->decoded.emoji = (uint32_t)this->packetHistory[i].emoji;
|
||||||
p->rx_rssi = this->packetHistory[i].rx_rssi;
|
p->rx_rssi = this->packetHistory[i].rx_rssi;
|
||||||
|
p->has_rx_rssi = this->packetHistory[i].has_rx_rssi; // presence captured at store time, not replay time
|
||||||
p->rx_snr = this->packetHistory[i].rx_snr;
|
p->rx_snr = this->packetHistory[i].rx_snr;
|
||||||
p->hop_start = this->packetHistory[i].hop_start;
|
p->hop_start = this->packetHistory[i].hop_start;
|
||||||
p->hop_limit = this->packetHistory[i].hop_limit;
|
p->hop_limit = this->packetHistory[i].hop_limit;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
struct PacketHistoryStruct {
|
struct PacketHistoryStruct {
|
||||||
uint32_t time;
|
uint32_t time;
|
||||||
|
bool has_rx_time; // whether `time` was a trustworthy epoch when captured, not a getTime() boot-relative fallback
|
||||||
uint32_t to;
|
uint32_t to;
|
||||||
uint32_t from;
|
uint32_t from;
|
||||||
uint32_t id;
|
uint32_t id;
|
||||||
@@ -21,6 +22,7 @@ struct PacketHistoryStruct {
|
|||||||
uint8_t payload[meshtastic_Constants_DATA_PAYLOAD_LEN];
|
uint8_t payload[meshtastic_Constants_DATA_PAYLOAD_LEN];
|
||||||
pb_size_t payload_size;
|
pb_size_t payload_size;
|
||||||
int32_t rx_rssi;
|
int32_t rx_rssi;
|
||||||
|
bool has_rx_rssi; // whether rx_rssi was a genuine measurement (e.g. not MQTT-relayed) when captured
|
||||||
float rx_snr;
|
float rx_snr;
|
||||||
uint8_t hop_start;
|
uint8_t hop_start;
|
||||||
uint8_t hop_limit;
|
uint8_t hop_limit;
|
||||||
|
|||||||
@@ -132,8 +132,13 @@ void HealthTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Display "Health From: ..." on its own
|
// Display "Health From: ..." on its own
|
||||||
|
char agoStr[16];
|
||||||
|
if (agoSecs == SINCE_UNKNOWN)
|
||||||
|
snprintf(agoStr, sizeof(agoStr), "?"); // no trustworthy arrival time to age against
|
||||||
|
else
|
||||||
|
snprintf(agoStr, sizeof(agoStr), "%us", (unsigned)agoSecs);
|
||||||
char headerStr[64];
|
char headerStr[64];
|
||||||
snprintf(headerStr, sizeof(headerStr), "Health From: %s(%ds)", lastSender, (int)agoSecs);
|
snprintf(headerStr, sizeof(headerStr), "Health From: %s(%s)", lastSender, agoStr);
|
||||||
display->drawString(x, y, headerStr);
|
display->drawString(x, y, headerStr);
|
||||||
|
|
||||||
char last_temp[16];
|
char last_temp[16];
|
||||||
|
|||||||
@@ -154,8 +154,13 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Display "Pow. From: ..."
|
// Display "Pow. From: ..."
|
||||||
|
char agoStr[16];
|
||||||
|
if (agoSecs == SINCE_UNKNOWN)
|
||||||
|
snprintf(agoStr, sizeof(agoStr), "?"); // no trustworthy arrival time to age against
|
||||||
|
else
|
||||||
|
snprintf(agoStr, sizeof(agoStr), "%us", (unsigned)agoSecs);
|
||||||
char fromStr[64];
|
char fromStr[64];
|
||||||
snprintf(fromStr, sizeof(fromStr), "Pow. From: %s (%us)", lastSender, agoSecs);
|
snprintf(fromStr, sizeof(fromStr), "Pow. From: %s (%s)", lastSender, agoStr);
|
||||||
display->drawString(x, graphics::getTextPositions(display)[line++], fromStr);
|
display->drawString(x, graphics::getTextPositions(display)[line++], fromStr);
|
||||||
|
|
||||||
// Display current and voltage based on ...power_metrics.has_[channel/voltage/current]... flags
|
// Display current and voltage based on ...power_metrics.has_[channel/voltage/current]... flags
|
||||||
|
|||||||
@@ -422,7 +422,11 @@ void TraceRouteModule::appendMyIDandSNR(meshtastic_RouteDiscovery *updated, floa
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (*snr_count < ROUTE_SIZE) {
|
if (*snr_count < ROUTE_SIZE) {
|
||||||
snr_list[*snr_count] = (int8_t)(snr * 4); // Convert SNR to 1 byte
|
// Clamp before the cast: q4-scaled SNR at or below the demodulation floor can reach
|
||||||
|
// -128 (=-32dB), which is bit-identical to the INT8_MIN "unknown SNR" sentinel used
|
||||||
|
// throughout this file. Reserve -128 for the sentinel; clamp real readings to -127.
|
||||||
|
int32_t q4 = clamp<int32_t>(lroundf(snr * 4.0f), -127, 127);
|
||||||
|
snr_list[*snr_count] = (int8_t)q4;
|
||||||
*snr_count += 1;
|
*snr_count += 1;
|
||||||
}
|
}
|
||||||
if (SNRonly)
|
if (SNRonly)
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ class AccelerometerThread : public concurrency::OSThread
|
|||||||
break;
|
break;
|
||||||
#if __has_include(<Adafruit_LIS3DH.h>)
|
#if __has_include(<Adafruit_LIS3DH.h>)
|
||||||
case ScanI2C::DeviceType::LIS3DH:
|
case ScanI2C::DeviceType::LIS3DH:
|
||||||
|
case ScanI2C::DeviceType::SC7A20:
|
||||||
sensor = new LIS3DHSensor(device);
|
sensor = new LIS3DHSensor(device);
|
||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -7,14 +7,16 @@ LIS3DHSensor::LIS3DHSensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::Mot
|
|||||||
|
|
||||||
bool LIS3DHSensor::init()
|
bool LIS3DHSensor::init()
|
||||||
{
|
{
|
||||||
if (sensor.begin(deviceAddress())) {
|
// The SC7A20 shares the register map but identifies as 0x11.
|
||||||
|
const uint8_t whoAmI = (deviceType() == ScanI2C::DeviceType::SC7A20) ? 0x11 : 0x33;
|
||||||
|
if (sensor.begin(deviceAddress(), whoAmI)) {
|
||||||
sensor.setRange(LIS3DH_RANGE_2_G);
|
sensor.setRange(LIS3DH_RANGE_2_G);
|
||||||
// Adjust threshold, higher numbers are less sensitive
|
// Adjust threshold, higher numbers are less sensitive
|
||||||
sensor.setClick(config.device.double_tap_as_button_press ? 2 : 1, MOTION_SENSOR_CHECK_INTERVAL_MS);
|
sensor.setClick(config.device.double_tap_as_button_press ? 2 : 1, MOTION_SENSOR_CHECK_INTERVAL_MS);
|
||||||
LOG_DEBUG("LIS3DH init ok");
|
LOG_DEBUG("%s init ok", deviceType() == ScanI2C::DeviceType::SC7A20 ? "SC7A20" : "LIS3DH");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
LOG_DEBUG("LIS3DH init failed");
|
LOG_DEBUG("%s init failed", deviceType() == ScanI2C::DeviceType::SC7A20 ? "SC7A20" : "LIS3DH");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -637,6 +637,21 @@ class NimbleBluetoothFromRadioCallback : public BLECharacteristicCallbacks
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// One log notify per log line shares the msys_1 mbuf pool with the fromNum doorbell and ATT
|
||||||
|
// responses, so a logging burst starves them; back off on rejection until the pool refills.
|
||||||
|
static constexpr uint32_t LOG_NOTIFY_BACKOFF_MS = 250;
|
||||||
|
static std::atomic<uint32_t> lastLogNotifyFailureMs{0};
|
||||||
|
|
||||||
|
class NimbleBluetoothLogRadioCallback : public BLECharacteristicCallbacks
|
||||||
|
{
|
||||||
|
void onStatus(BLECharacteristic *, Status s, uint32_t) override
|
||||||
|
{
|
||||||
|
// ERROR_GATT is the only status meaning the host refused it; the rest never allocated.
|
||||||
|
if (s == Status::ERROR_GATT)
|
||||||
|
lastLogNotifyFailureMs.store(millis());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
class NimbleBluetoothSecurityCallback : public BLESecurityCallbacks
|
class NimbleBluetoothSecurityCallback : public BLESecurityCallbacks
|
||||||
{
|
{
|
||||||
void onPassKeyNotify(uint32_t passkey) override
|
void onPassKeyNotify(uint32_t passkey) override
|
||||||
@@ -1005,6 +1020,9 @@ void NimbleBluetooth::setupService()
|
|||||||
static NimbleBluetoothFromRadioCallback fromRadioCallbacks;
|
static NimbleBluetoothFromRadioCallback fromRadioCallbacks;
|
||||||
FromRadioCharacteristic->setCallbacks(&fromRadioCallbacks);
|
FromRadioCharacteristic->setCallbacks(&fromRadioCallbacks);
|
||||||
|
|
||||||
|
static NimbleBluetoothLogRadioCallback logRadioCallbacks;
|
||||||
|
logRadioCharacteristic->setCallbacks(&logRadioCallbacks);
|
||||||
|
|
||||||
bleService->start();
|
bleService->start();
|
||||||
|
|
||||||
// Setup the battery service
|
// Setup the battery service
|
||||||
@@ -1056,6 +1074,11 @@ void NimbleBluetooth::sendLog(const uint8_t *logMessage, size_t length)
|
|||||||
if (!isConnected() || length > 512) {
|
if (!isConnected() || length > 512) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!logRadioCharacteristic) // BLE may have been torn down; never notify a freed characteristic
|
||||||
|
return;
|
||||||
|
// Pool still under pressure; drop this line rather than spend a buffer fromNum needs.
|
||||||
|
if (Throttle::isWithinTimespanMs(lastLogNotifyFailureMs.load(), LOG_NOTIFY_BACKOFF_MS))
|
||||||
|
return;
|
||||||
logRadioCharacteristic->setValue(logMessage, length);
|
logRadioCharacteristic->setValue(logMessage, length);
|
||||||
logRadioCharacteristic->notify();
|
logRadioCharacteristic->notify();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,6 +198,8 @@
|
|||||||
#define HW_VENDOR meshtastic_HardwareModel_T_DECK_PRO
|
#define HW_VENDOR meshtastic_HardwareModel_T_DECK_PRO
|
||||||
#elif defined(T_BEAM_1W)
|
#elif defined(T_BEAM_1W)
|
||||||
#define HW_VENDOR meshtastic_HardwareModel_TBEAM_1_WATT
|
#define HW_VENDOR meshtastic_HardwareModel_TBEAM_1_WATT
|
||||||
|
#elif defined(T_BEAM_BPF)
|
||||||
|
#define HW_VENDOR meshtastic_HardwareModel_TBEAM_BPF
|
||||||
#elif defined(T_LORA_PAGER)
|
#elif defined(T_LORA_PAGER)
|
||||||
#define HW_VENDOR meshtastic_HardwareModel_T_LORA_PAGER
|
#define HW_VENDOR meshtastic_HardwareModel_T_LORA_PAGER
|
||||||
#elif defined(HELTEC_V4)
|
#elif defined(HELTEC_V4)
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ class NRF52CryptoEngine : public CryptoEngine
|
|||||||
} else if (_key.length > 0) {
|
} else if (_key.length > 0) {
|
||||||
nRFCrypto.begin();
|
nRFCrypto.begin();
|
||||||
nRFCrypto_AES ctx;
|
nRFCrypto_AES ctx;
|
||||||
uint8_t myLen = ctx.blockLen(numBytes);
|
// Must not be uint8_t: blockLen() rounds up to the AES block size, so numBytes in
|
||||||
|
// 241..256 yields 256, which truncates to 0 and leaves Process() writing numBytes
|
||||||
|
// attacker-controlled bytes onto a zero-length stack buffer.
|
||||||
|
size_t myLen = ctx.blockLen(numBytes);
|
||||||
char encBuf[myLen] = {0};
|
char encBuf[myLen] = {0};
|
||||||
ctx.begin();
|
ctx.begin();
|
||||||
ctx.Process((char *)bytes, numBytes, _nonce, _key.bytes, _key.length, encBuf, ctx.encryptFlag, ctx.ctrMode);
|
ctx.Process((char *)bytes, numBytes, _nonce, _key.bytes, _key.length, encBuf, ctx.encryptFlag, ctx.ctrMode);
|
||||||
|
|||||||
@@ -99,6 +99,8 @@
|
|||||||
#define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M6
|
#define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M6
|
||||||
#elif defined(ELECROW_ThinkNode_M4)
|
#elif defined(ELECROW_ThinkNode_M4)
|
||||||
#define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M4
|
#define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M4
|
||||||
|
#elif defined(ELECROW_ThinkNode_M8)
|
||||||
|
#define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M8
|
||||||
#elif defined(NANO_G2_ULTRA)
|
#elif defined(NANO_G2_ULTRA)
|
||||||
#define HW_VENDOR meshtastic_HardwareModel_NANO_G2_ULTRA
|
#define HW_VENDOR meshtastic_HardwareModel_NANO_G2_ULTRA
|
||||||
#elif defined(CANARYONE)
|
#elif defined(CANARYONE)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifndef ARCH_PORTDUINO_WASM
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// Validates the loaded meshtasticd YAML, prints a report, and returns an exit code: 0 when
|
||||||
|
// nothing fatal was found, 1 otherwise. configFiles lists every attempted file in load order.
|
||||||
|
int runConfigCheck(const std::vector<std::string> &configFiles);
|
||||||
|
|
||||||
|
#endif // !ARCH_PORTDUINO_WASM
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "sleep.h"
|
#include "sleep.h"
|
||||||
#include "target_specific.h"
|
#include "target_specific.h"
|
||||||
|
|
||||||
|
#include "ConfigCheck.h"
|
||||||
#include "PortduinoGlue.h"
|
#include "PortduinoGlue.h"
|
||||||
#include "SHA256.h"
|
#include "SHA256.h"
|
||||||
#include "api/ServerAPI.h"
|
#include "api/ServerAPI.h"
|
||||||
@@ -65,6 +66,9 @@ char *configPath = nullptr;
|
|||||||
char *optionMac = nullptr;
|
char *optionMac = nullptr;
|
||||||
bool verboseEnabled = false;
|
bool verboseEnabled = false;
|
||||||
bool yamlOnly = false;
|
bool yamlOnly = false;
|
||||||
|
bool configCheck = false;
|
||||||
|
// Every config file we attempted to load, in load order, for --check to report on.
|
||||||
|
std::vector<std::string> attemptedConfigFiles;
|
||||||
|
|
||||||
const char *argp_program_version = optstr(APP_VERSION);
|
const char *argp_program_version = optstr(APP_VERSION);
|
||||||
|
|
||||||
@@ -86,9 +90,16 @@ void updateBatteryLevel(uint8_t level) NOT_IMPLEMENTED("updateBatteryLevel");
|
|||||||
int TCPPort = SERVER_API_DEFAULT_PORT;
|
int TCPPort = SERVER_API_DEFAULT_PORT;
|
||||||
bool checkConfigPort = true;
|
bool checkConfigPort = true;
|
||||||
|
|
||||||
|
// Long-only option: argp treats any key above the printable ASCII range as having no
|
||||||
|
// single-character equivalent.
|
||||||
|
#define OPT_CONFIG_CHECK 1001
|
||||||
|
|
||||||
static error_t parse_opt(int key, char *arg, struct argp_state *state)
|
static error_t parse_opt(int key, char *arg, struct argp_state *state)
|
||||||
{
|
{
|
||||||
switch (key) {
|
switch (key) {
|
||||||
|
case OPT_CONFIG_CHECK:
|
||||||
|
configCheck = true;
|
||||||
|
break;
|
||||||
case 'p':
|
case 'p':
|
||||||
if (sscanf(arg, "%d", &TCPPort) < 1) {
|
if (sscanf(arg, "%d", &TCPPort) < 1) {
|
||||||
return ARGP_ERR_UNKNOWN;
|
return ARGP_ERR_UNKNOWN;
|
||||||
@@ -160,13 +171,15 @@ static void checkSpidevBufsiz()
|
|||||||
|
|
||||||
void portduinoCustomInit()
|
void portduinoCustomInit()
|
||||||
{
|
{
|
||||||
static struct argp_option options[] = {{"port", 'p', "PORT", 0, "The TCP port to use."},
|
static struct argp_option options[] = {
|
||||||
{"config", 'c', "CONFIG_PATH", 0, "Full path of the .yaml config file to use."},
|
{"port", 'p', "PORT", 0, "The TCP port to use."},
|
||||||
{"hwid", 'h', "HWID", 0, "The mac address to assign to this virtual machine"},
|
{"config", 'c', "CONFIG_PATH", 0, "Full path of the .yaml config file to use."},
|
||||||
{"sim", 's', 0, 0, "Run in Simulated radio mode"},
|
{"hwid", 'h', "HWID", 0, "The mac address to assign to this virtual machine"},
|
||||||
{"verbose", 'v', 0, 0, "Set log level to full debug"},
|
{"sim", 's', 0, 0, "Run in Simulated radio mode"},
|
||||||
{"output-yaml", 'y', 0, 0, "Output config yaml and exit"},
|
{"verbose", 'v', 0, 0, "Set log level to full debug"},
|
||||||
{0}};
|
{"output-yaml", 'y', 0, 0, "Output config yaml and exit"},
|
||||||
|
{"check", OPT_CONFIG_CHECK, 0, 0, "Check the configuration for problems, print a report, and exit"},
|
||||||
|
{0}};
|
||||||
static void *childArguments;
|
static void *childArguments;
|
||||||
static char doc[] = "Meshtastic native build.";
|
static char doc[] = "Meshtastic native build.";
|
||||||
static char args_doc[] = "...";
|
static char args_doc[] = "...";
|
||||||
@@ -310,40 +323,54 @@ void portduinoSetup()
|
|||||||
portduino_config.lora_module = use_simradio;
|
portduino_config.lora_module = use_simradio;
|
||||||
} else if (configPath != nullptr) {
|
} else if (configPath != nullptr) {
|
||||||
if (loadConfig(configPath)) {
|
if (loadConfig(configPath)) {
|
||||||
if (!yamlOnly)
|
if (!yamlOnly && !configCheck)
|
||||||
std::cout << "Using " << configPath << " as config file" << std::endl;
|
std::cout << "Using " << configPath << " as config file" << std::endl;
|
||||||
} else {
|
} else if (!configCheck) {
|
||||||
|
// In check mode the path is already in attemptedConfigFiles, so fall through
|
||||||
|
// to runConfigCheck() and let it report the parse error with a file and line.
|
||||||
std::cout << "Unable to use " << configPath << " as config file" << std::endl;
|
std::cout << "Unable to use " << configPath << " as config file" << std::endl;
|
||||||
exit(EXIT_FAILURE);
|
exit(EXIT_FAILURE);
|
||||||
}
|
}
|
||||||
} else if (access("config.yaml", R_OK) == 0) {
|
} else if (access("config.yaml", R_OK) == 0) {
|
||||||
if (loadConfig("config.yaml")) {
|
if (loadConfig("config.yaml")) {
|
||||||
if (!yamlOnly)
|
if (!yamlOnly && !configCheck)
|
||||||
std::cout << "Using local config.yaml as config file" << std::endl;
|
std::cout << "Using local config.yaml as config file" << std::endl;
|
||||||
} else {
|
} else if (!configCheck) {
|
||||||
std::cout << "Unable to use local config.yaml as config file" << std::endl;
|
std::cout << "Unable to use local config.yaml as config file" << std::endl;
|
||||||
exit(EXIT_FAILURE);
|
exit(EXIT_FAILURE);
|
||||||
}
|
}
|
||||||
} else if (access("/etc/meshtasticd/config.yaml", R_OK) == 0) {
|
} else if (access("/etc/meshtasticd/config.yaml", R_OK) == 0) {
|
||||||
if (loadConfig("/etc/meshtasticd/config.yaml")) {
|
if (loadConfig("/etc/meshtasticd/config.yaml")) {
|
||||||
if (!yamlOnly)
|
if (!yamlOnly && !configCheck)
|
||||||
std::cout << "Using /etc/meshtasticd/config.yaml as config file" << std::endl;
|
std::cout << "Using /etc/meshtasticd/config.yaml as config file" << std::endl;
|
||||||
} else {
|
} else if (!configCheck) {
|
||||||
std::cout << "Unable to use /etc/meshtasticd/config.yaml as config file" << std::endl;
|
std::cout << "Unable to use /etc/meshtasticd/config.yaml as config file" << std::endl;
|
||||||
exit(EXIT_FAILURE);
|
exit(EXIT_FAILURE);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!yamlOnly)
|
if (!yamlOnly && !configCheck)
|
||||||
std::cout << "No 'config.yaml' found..." << std::endl;
|
std::cout << "No 'config.yaml' found..." << std::endl;
|
||||||
portduino_config.lora_module = use_simradio;
|
portduino_config.lora_module = use_simradio;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (portduino_config.config_directory != "") {
|
if (portduino_config.config_directory != "") {
|
||||||
std::string filetype = ".yaml";
|
// The throwing form of directory_iterator turns an unreadable ConfigDirectory into an
|
||||||
for (const std::filesystem::directory_entry &entry :
|
// uncaught filesystem_error and a SIGABRT, so take the error_code overload instead.
|
||||||
std::filesystem::directory_iterator{portduino_config.config_directory}) {
|
std::error_code dirError;
|
||||||
|
std::filesystem::directory_iterator entries{portduino_config.config_directory, dirError};
|
||||||
|
if (dirError) {
|
||||||
|
// Half a configuration is worse than none. --check continues so the report can say
|
||||||
|
// so with the rest of the findings.
|
||||||
|
if (!configCheck) {
|
||||||
|
std::cout << "Unable to read ConfigDirectory " << portduino_config.config_directory << ": " << dirError.message()
|
||||||
|
<< std::endl;
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const std::filesystem::directory_entry &entry : entries) {
|
||||||
if (ends_with(entry.path().string(), ".yaml")) {
|
if (ends_with(entry.path().string(), ".yaml")) {
|
||||||
std::cout << "Also using " << entry << " as additional config file" << std::endl;
|
if (!configCheck)
|
||||||
|
std::cout << "Also using " << entry << " as additional config file" << std::endl;
|
||||||
// .string() rather than .c_str(): path::value_type is wchar_t on
|
// .string() rather than .c_str(): path::value_type is wchar_t on
|
||||||
// Windows, and loadConfig() takes a const char *.
|
// Windows, and loadConfig() takes a const char *.
|
||||||
loadConfig(entry.path().string().c_str());
|
loadConfig(entry.path().string().c_str());
|
||||||
@@ -352,6 +379,11 @@ void portduinoSetup()
|
|||||||
}
|
}
|
||||||
|
|
||||||
#ifndef ARCH_PORTDUINO_WASM
|
#ifndef ARCH_PORTDUINO_WASM
|
||||||
|
// --check wins over --output-yaml: asking for validation and getting a config dump
|
||||||
|
// with no report at all would be the more surprising of the two outcomes.
|
||||||
|
if (configCheck)
|
||||||
|
exit(runConfigCheck(attemptedConfigFiles));
|
||||||
|
|
||||||
if (yamlOnly) {
|
if (yamlOnly) {
|
||||||
std::cout << portduino_config.emit_yaml() << std::endl;
|
std::cout << portduino_config.emit_yaml() << std::endl;
|
||||||
exit(EXIT_SUCCESS);
|
exit(EXIT_SUCCESS);
|
||||||
@@ -828,6 +860,10 @@ bool loadConfig(const char *configPath)
|
|||||||
#else
|
#else
|
||||||
bool loadConfig(const char *configPath)
|
bool loadConfig(const char *configPath)
|
||||||
{
|
{
|
||||||
|
// Recorded even when the load below fails: an unparseable config.d entry is skipped and its
|
||||||
|
// return value discarded by the caller, so --check needs to know it was attempted.
|
||||||
|
attemptedConfigFiles.push_back(configPath);
|
||||||
|
|
||||||
YAML::Node yamlConfig;
|
YAML::Node yamlConfig;
|
||||||
try {
|
try {
|
||||||
yamlConfig = YAML::LoadFile(configPath);
|
yamlConfig = YAML::LoadFile(configPath);
|
||||||
@@ -886,7 +922,9 @@ bool loadConfig(const char *configPath)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!found) {
|
if (!found && !configCheck) {
|
||||||
|
// --check names the valid modules in its report; exiting here would
|
||||||
|
// replace that with a bare one-liner.
|
||||||
std::cerr << "Unknown Lora.Module: " << moduleName << std::endl;
|
std::cerr << "Unknown Lora.Module: " << moduleName << std::endl;
|
||||||
exit(EXIT_FAILURE);
|
exit(EXIT_FAILURE);
|
||||||
}
|
}
|
||||||
@@ -899,6 +937,10 @@ bool loadConfig(const char *configPath)
|
|||||||
portduino_config.lr1110_max_power = yamlConfig["Lora"]["LR1110_MAX_POWER"].as<int>(22);
|
portduino_config.lr1110_max_power = yamlConfig["Lora"]["LR1110_MAX_POWER"].as<int>(22);
|
||||||
if (yamlConfig["Lora"]["LR1120_MAX_POWER"])
|
if (yamlConfig["Lora"]["LR1120_MAX_POWER"])
|
||||||
portduino_config.lr1120_max_power = yamlConfig["Lora"]["LR1120_MAX_POWER"].as<int>(13);
|
portduino_config.lr1120_max_power = yamlConfig["Lora"]["LR1120_MAX_POWER"].as<int>(13);
|
||||||
|
if (yamlConfig["Lora"]["LR2021_MAX_POWER"])
|
||||||
|
portduino_config.lr2021_max_power = yamlConfig["Lora"]["LR2021_MAX_POWER"].as<int>(22);
|
||||||
|
if (yamlConfig["Lora"]["LR2021_MAX_POWER_HF"])
|
||||||
|
portduino_config.lr2021_max_power_hf = yamlConfig["Lora"]["LR2021_MAX_POWER_HF"].as<int>(12);
|
||||||
if (yamlConfig["Lora"]["RF95_MAX_POWER"])
|
if (yamlConfig["Lora"]["RF95_MAX_POWER"])
|
||||||
portduino_config.rf95_max_power = yamlConfig["Lora"]["RF95_MAX_POWER"].as<int>(20);
|
portduino_config.rf95_max_power = yamlConfig["Lora"]["RF95_MAX_POWER"].as<int>(20);
|
||||||
|
|
||||||
@@ -1077,7 +1119,9 @@ bool loadConfig(const char *configPath)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#if !defined(HAS_HUB75_NATIVE)
|
#if !defined(HAS_HUB75_NATIVE)
|
||||||
if (portduino_config.displayPanel == hub75) {
|
if (portduino_config.displayPanel == hub75 && !configCheck) {
|
||||||
|
// --check still validates the rest of the file and reports this as a
|
||||||
|
// finding, so it must not exit from inside the load.
|
||||||
std::cerr << "HUB75 display panel selected, but this build does not support HUB75" << std::endl;
|
std::cerr << "HUB75 display panel selected, but this build does not support HUB75" << std::endl;
|
||||||
exit(EXIT_FAILURE);
|
exit(EXIT_FAILURE);
|
||||||
}
|
}
|
||||||
@@ -1221,8 +1265,12 @@ bool loadConfig(const char *configPath)
|
|||||||
(yamlConfig["General"]["AvailableDirectory"]).as<std::string>("/etc/meshtasticd/available.d/");
|
(yamlConfig["General"]["AvailableDirectory"]).as<std::string>("/etc/meshtasticd/available.d/");
|
||||||
if ((yamlConfig["General"]["MACAddress"]).as<std::string>("") != "" &&
|
if ((yamlConfig["General"]["MACAddress"]).as<std::string>("") != "" &&
|
||||||
(yamlConfig["General"]["MACAddressSource"]).as<std::string>("") != "") {
|
(yamlConfig["General"]["MACAddressSource"]).as<std::string>("") != "") {
|
||||||
std::cout << "Cannot set both MACAddress and MACAddressSource!" << std::endl;
|
// --check reports this as a finding against the file it came from, so
|
||||||
exit(EXIT_FAILURE);
|
// exiting here would kill the report before it is printed.
|
||||||
|
if (!configCheck) {
|
||||||
|
std::cout << "Cannot set both MACAddress and MACAddressSource!" << std::endl;
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (checkConfigPort) {
|
if (checkConfigPort) {
|
||||||
portduino_config.api_port = (yamlConfig["General"]["APIPort"]).as<int>(-1);
|
portduino_config.api_port = (yamlConfig["General"]["APIPort"]).as<int>(-1);
|
||||||
@@ -1245,7 +1293,10 @@ bool loadConfig(const char *configPath)
|
|||||||
portduino_config.mac_address.end());
|
portduino_config.mac_address.end());
|
||||||
}
|
}
|
||||||
} catch (YAML::Exception &e) {
|
} catch (YAML::Exception &e) {
|
||||||
std::cout << "*** Exception " << e.what() << std::endl;
|
// The check report repeats this against the file it came from, so printing it
|
||||||
|
// here too would only put a stray line above the report.
|
||||||
|
if (!configCheck)
|
||||||
|
std::cout << "*** Exception " << e.what() << std::endl;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ enum lora_module_enum {
|
|||||||
use_lr1110,
|
use_lr1110,
|
||||||
use_lr1120,
|
use_lr1120,
|
||||||
use_lr1121,
|
use_lr1121,
|
||||||
use_llcc68
|
use_llcc68,
|
||||||
|
use_lr2021
|
||||||
};
|
};
|
||||||
|
|
||||||
struct pinMapping {
|
struct pinMapping {
|
||||||
@@ -76,9 +77,10 @@ std::string exec(const char *cmd);
|
|||||||
|
|
||||||
extern struct portduino_config_struct {
|
extern struct portduino_config_struct {
|
||||||
// Lora
|
// Lora
|
||||||
std::map<lora_module_enum, std::string> loraModules = {
|
std::map<lora_module_enum, std::string> loraModules = {{use_simradio, "sim"}, {use_autoconf, "auto"}, {use_rf95, "RF95"},
|
||||||
{use_simradio, "sim"}, {use_autoconf, "auto"}, {use_rf95, "RF95"}, {use_sx1262, "sx1262"}, {use_sx1268, "sx1268"},
|
{use_sx1262, "sx1262"}, {use_sx1268, "sx1268"}, {use_sx1280, "sx1280"},
|
||||||
{use_sx1280, "sx1280"}, {use_lr1110, "lr1110"}, {use_lr1120, "lr1120"}, {use_lr1121, "lr1121"}, {use_llcc68, "LLCC68"}};
|
{use_lr1110, "lr1110"}, {use_lr1120, "lr1120"}, {use_lr1121, "lr1121"},
|
||||||
|
{use_llcc68, "LLCC68"}, {use_lr2021, "lr2021"}};
|
||||||
|
|
||||||
std::map<screen_modules, std::string> screen_names = {{x11, "X11"}, {fb, "FB"}, {st7789, "ST7789"},
|
std::map<screen_modules, std::string> screen_names = {{x11, "X11"}, {fb, "FB"}, {st7789, "ST7789"},
|
||||||
{st7735, "ST7735"}, {st7735s, "ST7735S"}, {st7796, "ST7796"},
|
{st7735, "ST7735"}, {st7735s, "ST7735S"}, {st7796, "ST7796"},
|
||||||
@@ -100,6 +102,8 @@ extern struct portduino_config_struct {
|
|||||||
int sx128x_max_power = 13;
|
int sx128x_max_power = 13;
|
||||||
int lr1110_max_power = 22;
|
int lr1110_max_power = 22;
|
||||||
int lr1120_max_power = 13;
|
int lr1120_max_power = 13;
|
||||||
|
int lr2021_max_power = 22;
|
||||||
|
int lr2021_max_power_hf = 12;
|
||||||
int rf95_max_power = 20;
|
int rf95_max_power = 20;
|
||||||
bool dio2_as_rf_switch = false;
|
bool dio2_as_rf_switch = false;
|
||||||
int dio3_tcxo_voltage = 0;
|
int dio3_tcxo_voltage = 0;
|
||||||
@@ -287,6 +291,10 @@ extern struct portduino_config_struct {
|
|||||||
out << YAML::Key << "LR1110_MAX_POWER" << YAML::Value << lr1110_max_power;
|
out << YAML::Key << "LR1110_MAX_POWER" << YAML::Value << lr1110_max_power;
|
||||||
if (lr1120_max_power != 13)
|
if (lr1120_max_power != 13)
|
||||||
out << YAML::Key << "LR1120_MAX_POWER" << YAML::Value << lr1120_max_power;
|
out << YAML::Key << "LR1120_MAX_POWER" << YAML::Value << lr1120_max_power;
|
||||||
|
if (lr2021_max_power != 22)
|
||||||
|
out << YAML::Key << "LR2021_MAX_POWER" << YAML::Value << lr2021_max_power;
|
||||||
|
if (lr2021_max_power_hf != 12)
|
||||||
|
out << YAML::Key << "LR2021_MAX_POWER_HF" << YAML::Value << lr2021_max_power_hf;
|
||||||
if (rf95_max_power != 20)
|
if (rf95_max_power != 20)
|
||||||
out << YAML::Key << "RF95_MAX_POWER" << YAML::Value << rf95_max_power;
|
out << YAML::Key << "RF95_MAX_POWER" << YAML::Value << rf95_max_power;
|
||||||
|
|
||||||
|
|||||||
@@ -420,13 +420,16 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
|
|||||||
}
|
}
|
||||||
|
|
||||||
jsonObj["id"] = (Json::UInt)mp->id;
|
jsonObj["id"] = (Json::UInt)mp->id;
|
||||||
jsonObj["timestamp"] = (Json::UInt)mp->rx_time;
|
// Emit 0 rather than leak a millis() placeholder when has_rx_time is false.
|
||||||
|
jsonObj["timestamp"] = mp->has_rx_time ? (Json::UInt)mp->rx_time : 0;
|
||||||
jsonObj["to"] = (Json::UInt)mp->to;
|
jsonObj["to"] = (Json::UInt)mp->to;
|
||||||
jsonObj["from"] = (Json::UInt)mp->from;
|
jsonObj["from"] = (Json::UInt)mp->from;
|
||||||
jsonObj["channel"] = (Json::UInt)mp->channel;
|
jsonObj["channel"] = (Json::UInt)mp->channel;
|
||||||
jsonObj["type"] = msgType;
|
jsonObj["type"] = msgType;
|
||||||
jsonObj["sender"] = nodeDB->getNodeId();
|
jsonObj["sender"] = nodeDB->getNodeId();
|
||||||
if (mp->rx_rssi != 0)
|
// rx_rssi has explicit presence on the wire (unlike rx_snr): trust has_rx_rssi rather than a
|
||||||
|
// != 0 heuristic, since 0 dBm is a legitimate reading on some radios.
|
||||||
|
if (mp->has_rx_rssi)
|
||||||
jsonObj["rssi"] = (int)mp->rx_rssi;
|
jsonObj["rssi"] = (int)mp->rx_rssi;
|
||||||
if (mp->rx_snr != 0)
|
if (mp->rx_snr != 0)
|
||||||
jsonObj["snr"] = (float)mp->rx_snr;
|
jsonObj["snr"] = (float)mp->rx_snr;
|
||||||
@@ -450,13 +453,16 @@ std::string MeshPacketSerializer::JsonSerializeEncrypted(const meshtastic_MeshPa
|
|||||||
|
|
||||||
jsonObj["id"] = (Json::UInt)mp->id;
|
jsonObj["id"] = (Json::UInt)mp->id;
|
||||||
jsonObj["time_ms"] = (double)millis();
|
jsonObj["time_ms"] = (double)millis();
|
||||||
jsonObj["timestamp"] = (Json::UInt)mp->rx_time;
|
// Emit 0 rather than leak a millis() placeholder when has_rx_time is false.
|
||||||
|
jsonObj["timestamp"] = mp->has_rx_time ? (Json::UInt)mp->rx_time : 0;
|
||||||
jsonObj["to"] = (Json::UInt)mp->to;
|
jsonObj["to"] = (Json::UInt)mp->to;
|
||||||
jsonObj["from"] = (Json::UInt)mp->from;
|
jsonObj["from"] = (Json::UInt)mp->from;
|
||||||
jsonObj["channel"] = (Json::UInt)mp->channel;
|
jsonObj["channel"] = (Json::UInt)mp->channel;
|
||||||
jsonObj["want_ack"] = mp->want_ack;
|
jsonObj["want_ack"] = mp->want_ack;
|
||||||
|
|
||||||
if (mp->rx_rssi != 0)
|
// rx_rssi has explicit presence on the wire (unlike rx_snr): trust has_rx_rssi rather than a
|
||||||
|
// != 0 heuristic, since 0 dBm is a legitimate reading on some radios.
|
||||||
|
if (mp->has_rx_rssi)
|
||||||
jsonObj["rssi"] = (int)mp->rx_rssi;
|
jsonObj["rssi"] = (int)mp->rx_rssi;
|
||||||
if (mp->rx_snr != 0)
|
if (mp->rx_snr != 0)
|
||||||
jsonObj["snr"] = (float)mp->rx_snr;
|
jsonObj["snr"] = (float)mp->rx_snr;
|
||||||
|
|||||||
@@ -386,6 +386,20 @@ A well-structured test suite follows this pattern:
|
|||||||
4. **Lifecycle tests** - state persistence, startup from blank, restart recovery
|
4. **Lifecycle tests** - state persistence, startup from blank, restart recovery
|
||||||
5. **Summary test** (optional) - emits a scenario table into the log for quick CI review
|
5. **Summary test** (optional) - emits a scenario table into the log for quick CI review
|
||||||
|
|
||||||
|
## Not a Unity suite: `bin/test-config-check.sh`
|
||||||
|
|
||||||
|
Portduino YAML validation is tested by driving a built `meshtasticd` rather than by a
|
||||||
|
Unity suite, because what it asserts - the exit status and printed report of
|
||||||
|
`meshtasticd --check`, and the fact that a normal run still refuses a bad config - are
|
||||||
|
properties of the process, not of a linkable function. Fixtures live in
|
||||||
|
`test/fixtures/portduino-config/` (see the README there); CI runs it in
|
||||||
|
`test_native.yml`. It is not counted in `native-suite-count`, which only tracks `test_*`
|
||||||
|
directories.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pio run -e native && ./bin/test-config-check.sh
|
||||||
|
```
|
||||||
|
|
||||||
## Existing Test Suites
|
## Existing Test Suites
|
||||||
|
|
||||||
| Suite | Module Under Test |
|
| Suite | Module Under Test |
|
||||||
|
|||||||
+158
@@ -0,0 +1,158 @@
|
|||||||
|
# Portduino config fixtures
|
||||||
|
|
||||||
|
Input files for `bin/test-config-check.sh`, which drives a built `meshtasticd`
|
||||||
|
binary and asserts what `meshtasticd --check` reports about each one. Every file
|
||||||
|
here is referenced by name from that script, so renaming one means editing it too.
|
||||||
|
|
||||||
|
The theme is configuration that is accepted by the YAML parser but does not mean
|
||||||
|
what it looks like it means - the failures that otherwise only show up as a radio
|
||||||
|
that never transmits.
|
||||||
|
|
||||||
|
Each file carries a comment header naming its planted fault and what the checker is
|
||||||
|
expected to say about it, so a fixture can be read on its own. One assertion,
|
||||||
|
`malformed-indent.yaml`'s reported line number, counts those header lines - editing
|
||||||
|
that file's comments means updating the expected line in the script.
|
||||||
|
|
||||||
|
These files are exempt from `trunk fmt` (see `.trunk/trunk.yaml`): prettier rejects
|
||||||
|
the duplicate keys and bad indentation that are the entire point of them.
|
||||||
|
|
||||||
|
## Valid, one per radio module family
|
||||||
|
|
||||||
|
These must each report zero errors and zero warnings, and must resolve to the
|
||||||
|
module named in the file. A silent fallback to `sim` would otherwise pass.
|
||||||
|
|
||||||
|
| File | Module | Family |
|
||||||
|
| -------------------- | -------- | ---------- |
|
||||||
|
| `module-rf95.yaml` | `RF95` | SX127x |
|
||||||
|
| `module-sx1262.yaml` | `sx1262` | SX126x |
|
||||||
|
| `module-sx1268.yaml` | `sx1268` | SX126x |
|
||||||
|
| `module-llcc68.yaml` | `LLCC68` | SX126x |
|
||||||
|
| `module-sx1280.yaml` | `sx1280` | SX128x |
|
||||||
|
| `module-lr1110.yaml` | `lr1110` | LR11xx |
|
||||||
|
| `module-lr1120.yaml` | `lr1120` | LR11xx |
|
||||||
|
| `module-lr1121.yaml` | `lr1121` | LR11xx |
|
||||||
|
| `module-sim.yaml` | `sim` | simulated |
|
||||||
|
| `module-auto.yaml` | `auto` | autodetect |
|
||||||
|
|
||||||
|
`valid.yaml` is a minimal SX126x config, and `empty-sections.yaml` (`Lora:` with
|
||||||
|
no body) exists to prove the checker does not invent a finding for it.
|
||||||
|
|
||||||
|
## Module naming
|
||||||
|
|
||||||
|
Names are matched exactly and are not consistently cased - `RF95` and `LLCC68`
|
||||||
|
are upper, `sx1262` and `lr1121` lower.
|
||||||
|
|
||||||
|
| File | Expected |
|
||||||
|
| ------------------------ | --------------------------------------------------- |
|
||||||
|
| `module-unknown.yaml` | `sx1263` is refused, and the valid set is listed. |
|
||||||
|
| `module-wrong-case.yaml` | `llcc68` is refused with a "did you mean" for case. |
|
||||||
|
|
||||||
|
## LR11xx rfswitch table
|
||||||
|
|
||||||
|
| File | Expected |
|
||||||
|
| ------------------------------ | ----------------------------------------------------------------- |
|
||||||
|
| `rfswitch-valid.yaml` | A full seven-mode table on an `lr1121` is clean. |
|
||||||
|
| `rfswitch-partial.yaml` | Legal, but the omitted modes are named - they are driven all-LOW. |
|
||||||
|
| `rfswitch-bad-pin.yaml` | `DIO9` is not one of DIO5/6/7/8/10. |
|
||||||
|
| `rfswitch-row-length.yaml` | Rows shorter and longer than the declared pin count. |
|
||||||
|
| `rfswitch-bad-level.yaml` | `high` and `On`: anything not exactly `HIGH` is silently LOW. |
|
||||||
|
| `rfswitch-no-pins.yaml` | No `pins` list, so no switch pin is ever driven. |
|
||||||
|
| `rfswitch-too-many-pins.yaml` | Six pins declared; only the first five are read. |
|
||||||
|
| `rfswitch-not-a-map.yaml` | `rfswitch_table` given a scalar. |
|
||||||
|
| `rfswitch-unknown-mode.yaml` | `MODE_TRANSMIT` is not a mode. |
|
||||||
|
| `rfswitch-stranded-modes.yaml` | A `MODE_` row one level out, sitting under `Lora:` doing nothing. |
|
||||||
|
|
||||||
|
`rfswitch-partial.yaml` is legal but noted: the omitted modes are driven all-LOW.
|
||||||
|
`module-mismatch-lr11xx.yaml` (LR11xx with no table - cannot transmit) and
|
||||||
|
`module-mismatch-sx126x.yaml` (a table on a radio that never applies one) cover
|
||||||
|
the module/table disagreement in both directions.
|
||||||
|
|
||||||
|
## PA gain table (`TX_GAIN_LORA`)
|
||||||
|
|
||||||
|
Two shapes are accepted and they fail differently. A list is read element-by-element
|
||||||
|
with `.as<int>()` and NO fallback, so one bad entry throws and meshtasticd will not
|
||||||
|
start. A bare scalar is read as `.as<int>(0)` and merely falls back to 0. The table
|
||||||
|
is `uint16_t[22]`, so extra points are dropped and out-of-range values wrap.
|
||||||
|
|
||||||
|
| File | Expected |
|
||||||
|
| ---------------------------- | ------------------------------------------------------------------------- |
|
||||||
|
| `txgain-scalar.yaml` | **Clean, and a regression guard** - an earlier checker called this fatal. |
|
||||||
|
| `value-type-fatal-list.yaml` | A non-numeric list entry: throws, so meshtasticd will not start. |
|
||||||
|
| `txgain-out-of-range.yaml` | `-5` and `70000` wrap to a different gain than written. |
|
||||||
|
| `txgain-too-many.yaml` | 25 points; everything past the 22nd is dropped. |
|
||||||
|
|
||||||
|
## Value types, ranges and units
|
||||||
|
|
||||||
|
| File | Expected |
|
||||||
|
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `value-type-fatal.yaml` | `Logging.AsciiLogs` is the other no-fallback read: a bad value stops meshtasticd starting. |
|
||||||
|
| `value-type-silent.yaml` | Wrong-typed values where the read has a default: silently replaced, so the setting does nothing. |
|
||||||
|
| `tcxo-millivolts.yaml` | `DIO3_TCXO_VOLTAGE` is in VOLTS and multiplied by 1000, so `1800` silently asks for 1800V. Write `1.8`. |
|
||||||
|
| `port-out-of-range.yaml` | `APIPort` outside 1024-65535 is silently ignored; `Webserver.Port` has no guard at all. |
|
||||||
|
| `statusmessage-long.yaml` | Copied into a `char[80]`, so it is safe but silently shortened to 79 characters. |
|
||||||
|
| `configdir-missing.yaml` | **Crash regression guard** - an unreadable `ConfigDirectory` used to abort meshtasticd (and `--check`) with SIGABRT via an uncaught `filesystem_error`. |
|
||||||
|
|
||||||
|
## MAC address
|
||||||
|
|
||||||
|
The MAC no longer determines NodeNum - that comes from the public key - but a MAC
|
||||||
|
that fails to apply still falls through to the BlueZ and LoRa-serial fallbacks, and
|
||||||
|
if those yield nothing meshtasticd exits with "Blank MAC Address not allowed!".
|
||||||
|
|
||||||
|
| File | Expected |
|
||||||
|
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `mac-conflict.yaml` | Both `MACAddress` and `MACAddressSource`; meshtasticd refuses. |
|
||||||
|
| `mac-malformed.yaml` | `AA:BB:CC` is under 12 hex digits, so it is silently dropped. |
|
||||||
|
| `mac-source-missing.yaml` | Names an interface with no `/sys/class/net/<n>/address`. Warning, not an error: it is machine-dependent and may be checked on another host. |
|
||||||
|
|
||||||
|
## CH341 USB-SPI adapters
|
||||||
|
|
||||||
|
`spidev: ch341` is a different hardware model, not a variant of the same one. The Lora
|
||||||
|
pins become indexes on the adapter and are driven by the usermode USB driver -
|
||||||
|
`portduinoSetup()` skips `initGPIOPin()` for every one of them - so nothing is claimed
|
||||||
|
from a gpiochip. This is also the only shape that works on Windows and macOS, which have
|
||||||
|
no gpiochip, `gpiodetect` or `gpioinfo` to check anything against.
|
||||||
|
|
||||||
|
| File | Expected |
|
||||||
|
| --------------------- | ----------------------------------------------------------------------------- |
|
||||||
|
| `usb-ch341.yaml` | Clean, and the report lists adapter pins rather than resolved gpiochip lines. |
|
||||||
|
| `ch341-gpiochip.yaml` | A gpiochip and line mapping alongside `ch341`: read, stored, and never used. |
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
| File | Expected |
|
||||||
|
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
|
| `duplicate-key.yaml` | yaml-cpp keeps the FIRST duplicate, so the later value is lost. |
|
||||||
|
| `nonmap-section.yaml` | `Lora: invalid` - a known section whose body cannot be read. |
|
||||||
|
| `unknown-section.yaml` | A top-level section meshtasticd never reads. |
|
||||||
|
| `stranded-key.yaml` | `spidev` left at the top level instead of inside a section. |
|
||||||
|
| `top-level-list.yaml` | Document root is a sequence. |
|
||||||
|
| `empty-file.yaml` | No content: comments only, which parse to a null document. A warning, not an error. |
|
||||||
|
| `malformed-indent.yaml` | Will not parse; the report must still name the file and line. |
|
||||||
|
| `pin-unknown-subkey.yaml` | A pin mapping accepts only `pin`, `gpiochip` and `line`. |
|
||||||
|
| `pin-unreadable.yaml` | A non-numeric pin resolves to -1 and trips an assertion at startup. |
|
||||||
|
| `hub75-unknown-key.yaml` | An unknown `Display.HUB75` option. On a build without rgbmatrix this also reports the missing HUB75 support, so the test asserts only the unknown key. |
|
||||||
|
|
||||||
|
## Across a config directory
|
||||||
|
|
||||||
|
`configd-conflict/` is a whole tree: a `config.yaml` pointing at a `config.d/`
|
||||||
|
holding two more `Lora:` sections. It covers the trap that a key not repeated in
|
||||||
|
the last-loaded file is reset to its default - here `config.yaml` sets
|
||||||
|
`DIO3_TCXO_VOLTAGE: 1800` and the effective configuration ends up without it.
|
||||||
|
The load order within `config.d/` comes from the filesystem, so the report warns
|
||||||
|
rather than assuming alphabetical order.
|
||||||
|
|
||||||
|
`rfswitch-sticky/` covers the one place where "the file loaded last wins" is false,
|
||||||
|
and it documents a firmware bug rather than a configuration mistake. Its `config.d/`
|
||||||
|
holds two switch tables; the last one loaded sets `MODE_RX` LOW on both pins, but the
|
||||||
|
loader only ever writes HIGH and never writes LOW back, so the HIGH from the earlier
|
||||||
|
file survives and the effective table is the OR of both. Verified with
|
||||||
|
`meshtasticd --output-yaml`. Until the loader is fixed, `--check` reports this as an
|
||||||
|
error and tells you to enable exactly one.
|
||||||
|
|
||||||
|
## Running these as a normal boot
|
||||||
|
|
||||||
|
`malformed-indent.yaml`, `nonmap-section.yaml`, `module-unknown.yaml`,
|
||||||
|
`mac-conflict.yaml` and `hub75-unknown-key.yaml` are also run _without_ `--check`,
|
||||||
|
where each must be rejected with a non-zero exit. That is the guard on `--check`
|
||||||
|
mode not having quietly made the normal path permissive. No other fixture is run
|
||||||
|
that way: a config meshtasticd accepts makes it boot a node and block.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# FAULT: a CH341 config that also maps its pins onto a gpiochip. Every Lora pin is
|
||||||
|
# skipped by the GPIO claim loop when spidev is ch341, so the chip and line numbers below
|
||||||
|
# are read, stored, and never used - while reading as though they were authoritative.
|
||||||
|
Lora:
|
||||||
|
Module: sx1262
|
||||||
|
spidev: ch341
|
||||||
|
gpiochip: 4
|
||||||
|
CS:
|
||||||
|
pin: 0
|
||||||
|
gpiochip: 4
|
||||||
|
line: 21
|
||||||
|
IRQ: 6
|
||||||
|
Reset: 2
|
||||||
|
Busy: 4
|
||||||
|
USB_PID: 0x5512
|
||||||
|
USB_VID: 0x1A86
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Second of three files. Re-opens "Lora:", which is what resets the keys set in
|
||||||
|
# ../config.yaml that are not repeated here.
|
||||||
|
Lora:
|
||||||
|
Module: sx1262
|
||||||
|
CS: 21
|
||||||
|
IRQ: 16
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Third of three files, and the one that wins -- but only because the filesystem
|
||||||
|
# happens to return it last. config.d/ load order is not guaranteed to be
|
||||||
|
# alphabetical and can differ between machines, which is why the report warns
|
||||||
|
# rather than assuming an order.
|
||||||
|
Lora:
|
||||||
|
Module: lr1121
|
||||||
|
CS: 99
|
||||||
|
IRQ: 16
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# The primary config of a three-file set (see config.d/ alongside it).
|
||||||
|
# FAULT, and it is invisible in this file alone: two more files in config.d/ each open
|
||||||
|
# their own "Lora:" section, and the keys below that they do not repeat -- notably
|
||||||
|
# DIO3_TCXO_VOLTAGE -- are reset to their defaults when the last of them is loaded.
|
||||||
|
# The effective configuration ends up with no TCXO voltage at all.
|
||||||
|
General:
|
||||||
|
ConfigDirectory: config.d/
|
||||||
|
Lora:
|
||||||
|
Module: sx1262
|
||||||
|
spidev: spidev0.0
|
||||||
|
DIO3_TCXO_VOLTAGE: 1800
|
||||||
|
CS: 21
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# FAULT, and it used to be a hard crash: a ConfigDirectory that cannot be read made
|
||||||
|
# std::filesystem::directory_iterator throw an uncaught filesystem_error, aborting
|
||||||
|
# meshtasticd with SIGABRT -- and aborting --check along with it, so the one tool meant
|
||||||
|
# to diagnose the problem died on it. It now fails cleanly and is reported here.
|
||||||
|
Lora:
|
||||||
|
Module: sx1262
|
||||||
|
General:
|
||||||
|
ConfigDirectory: /nonexistent/does-not-exist/
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# FAULT: Lora.CS is defined twice. YAML forbids this but yaml-cpp does not complain:
|
||||||
|
# it keeps the FIRST occurrence and silently discards the rest, so the CS: 22 below
|
||||||
|
# looks applied and is not. Expect an ERROR naming both lines.
|
||||||
|
Lora:
|
||||||
|
Module: sx1262
|
||||||
|
CS: 21
|
||||||
|
CS: 22
|
||||||
|
IRQ: 16
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# FAULT: a zero-content file. Only comments here, which yaml-cpp parses to a null
|
||||||
|
# document -- the same as truly empty. Expect a WARNING, not an ERROR.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# CLEAN, and deliberately so. A section with no body is a null node, not a map.
|
||||||
|
# loadConfig() tests the section node before reading it, so this is harmless -- the
|
||||||
|
# checker must not report it. Regression guard for the null-vs-non-map distinction.
|
||||||
|
Lora:
|
||||||
|
Display:
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# FAULT: Colours is not a HUB75 option (the real ones are listed in ConfigCheck.cpp).
|
||||||
|
# On a meshtasticd built without rgbmatrix this also reports the missing HUB75
|
||||||
|
# support, so the test asserts only the unknown key -- the second finding depends on
|
||||||
|
# how the binary was built.
|
||||||
|
Lora:
|
||||||
|
Module: sx1262
|
||||||
|
Display:
|
||||||
|
Panel: HUB75
|
||||||
|
HUB75:
|
||||||
|
Rows: 64
|
||||||
|
Colours: 3
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# FAULT: MACAddress and MACAddressSource are mutually exclusive; meshtasticd exits
|
||||||
|
# rather than pick one. That exit is inside loadConfig(), so --check has to report it
|
||||||
|
# without letting the exit kill the report. Also run without --check, where
|
||||||
|
# meshtasticd must refuse to start.
|
||||||
|
General:
|
||||||
|
MACAddress: AA:BB:CC:DD:EE:FF
|
||||||
|
MACAddressSource: eth0
|
||||||
|
Lora:
|
||||||
|
Module: sx1262
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# FAULT: AA:BB:CC is three octets, not six. loadConfig() strips the colons and then
|
||||||
|
# requires more than 11 characters, so this is dropped without a word and the MAC
|
||||||
|
# comes from the Bluetooth adapter or the LoRa serial instead. NodeNum is unaffected
|
||||||
|
# (that comes from the public key), but if no fallback yields a MAC, meshtasticd
|
||||||
|
# exits with "Blank MAC Address not allowed!".
|
||||||
|
General:
|
||||||
|
MACAddress: AA:BB:CC
|
||||||
|
Lora:
|
||||||
|
Module: sx1262
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# FAULT: names a network interface that does not exist, so /sys/class/net/<n>/address
|
||||||
|
# reads back empty and no MAC is set. A WARNING rather than an ERROR: which interfaces
|
||||||
|
# exist is a property of the machine, and a config is often checked on another host.
|
||||||
|
General:
|
||||||
|
MACAddressSource: nosuchiface99
|
||||||
|
Lora:
|
||||||
|
Module: sx1262
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user