mirror of
https://github.com/meshtastic/firmware.git
synced 2026-09-23 06:45:24 -04:00
Merge branch 'develop' into mesh-pager-x2
This commit is contained in:
commit
76ca935ccc
208 files changed
+6074
-1786
No files matched your search
@@ -313,7 +313,7 @@ firmware/
|
||||
│ └── native/ # Linux/Portduino variants
|
||||
├── protobufs/ # Protocol buffer definitions
|
||||
├── boards/ # Custom PlatformIO board definitions
|
||||
├── test/ # Native unit-test suites (count: test/native-suite-count)
|
||||
├── test/ # Native unit-test suites (count = the test_* dirs, detected on the fly)
|
||||
└── bin/ # Build and utility scripts
|
||||
```
|
||||
|
||||
@@ -332,12 +332,24 @@ firmware/
|
||||
|
||||
- Follow existing code style - run `trunk fmt` before commits
|
||||
- Prefer `LOG_DEBUG`, `LOG_INFO`, `LOG_WARN`, `LOG_ERROR` for logging
|
||||
- **Three logging tiers for diagnostics.** `LOG_TRACE` is the per-packet/per-poll firehose - compiled out by default (`MESHTASTIC_TRACE_LOGGING=1` enables; always on for portduino). Subsystem bring-up detail routes through a per-subsystem gate macro instead, e.g. `LOG_DEBUG_GPS(...)` in `src/gps/GPSLog.h` (`GPS_DEBUG=1` enables; costs no flash when off) - model new subsystem gates on it or on `LOG_MIGRATION` (`src/mesh/WarmNodeStore.h`): `#ifndef` value-default, `#if SYM` value test, `((void)0)` off-branch. Genuine anomalies stay unconditional `LOG_WARN`/`LOG_ERROR`.
|
||||
- **Format node IDs and packet IDs as `0x%08x` in logs.** This covers `NodeNum`/`PacketId` and the `uint32_t` packet fields `from`, `to`, `id`, `dest`, `source`, `request_id`, and `node_id`. They are 32-bit, so 8 hex digits is exact - `%08x` never truncates or leaves a value ragged. Do **not** use `%x` (variable width) or `%0x` (a no-op typo for `%08x` - the `0` flag does nothing without a width). User-facing display uses `!%08x` (the `!xxxxxxxx` convention), e.g. `Applet::hexifyNodeNum`.
|
||||
- **Do not zero-pad one-byte values to 8.** `next_hop`, `relay_node`, and the next-hop hint are `uint8_t` last-byte route hints, and `channel` is a one-byte hash/index - log these as `0x%x` (or `%d`). Padding a byte to `0x000000ab` falsely implies a full node number. The same goes for I2C addresses, register values, flags/bitmasks, and error/reason codes: they are not IDs, so leave them `0x%x`.
|
||||
- Use `assert()` for invariants that should never fail
|
||||
- C++17 features are available (`std::optional`, structured bindings, `if constexpr`, etc.)
|
||||
- **Keep code comments minimal - one or two lines, max.** Comment only when the _why_ isn't obvious from the code; never restate what the next line does. No multi-paragraph block comments explaining straightforward changes. The diff and commit message carry the rationale; the code carries the behavior.
|
||||
- **Use `Throttle` for time-based rate limiting, not raw `millis()` math.** `src/mesh/Throttle.h` provides `Throttle::isWithinTimespanMs(lastMs, intervalMs)` (returns true while inside the cooldown) and `Throttle::execute(&lastMs, intervalMs, func)` (function-pointer form that updates the timestamp on fire). Use these for any "did N ms pass since X" check - raw `millis() > lastMs + N` is rollover-unsafe (breaks after ~49.7 days) and inconsistent with the rest of the codebase. The helpers compute `now - lastMs` with unsigned subtraction, which wraps correctly.
|
||||
- **Never compare against `millis()` directly. Use `Throttle`.** `src/mesh/Throttle.h` is the sanctioned way to ask about time, and CI enforces this (`millis-deadline-check` in `.github/workflows/test_native.yml` fails the PR on a new `millis() >` / `< millis()` comparison).
|
||||
- `Throttle::isWithinTimespanMs(lastMs, intervalMs)` - true while still inside the cooldown.
|
||||
- `Throttle::hasElapsed(lastMs, intervalMs)` - its complement, true once the interval has passed (inclusive `>=`). Prefer this to spelling `!isWithinTimespanMs(...)`.
|
||||
- `Throttle::execute(&lastMs, intervalMs, func)` - function-pointer form that updates the timestamp on fire.
|
||||
- `Throttle::deadlinePassed(deadlineMs)` - for a stored absolute deadline that cannot be re-expressed as "interval since an event". Uses an unsigned half-range compare; reads deadlines more than ~24.8 days out as already passed, which no interval in this firmware approaches (the longest is 24 h).
|
||||
- `Throttle::deadlinePassedAt(nowMs, deadlineMs)` - the same test against a caller-supplied `now`, for a loop that snapshots the clock once and then tests many deadlines (`NextHopRouter::doRetransmissions()`). Take the snapshot from `Time::getMillis()`, not `millis()`.
|
||||
|
||||
Raw `millis() > deadline` or `deadline < millis()` is rollover-unsafe: the comparison inverts while the deadline sits on the far side of the 32-bit wrap, so the action fires immediately (losing its whole wait) or blocks for roughly the interval it should have waited - days, for the nRF52 flash-corruption backoff. All five helpers subtract first, so unsigned wraparound cancels out. `Throttle` reads the clock through `Time::getMillis()` (`src/UptimeClock.h`), which means every one of its ~94 call sites is time-injectable - a native test can drive `Time::setTestMillis(0xFFFFFF00)` across the wrap. For _timestamps_ (not deadlines) there is `Time::getMillisMonotonic()` / `Time::getUptimeSecs()` - a 64-bit monotonic uptime read. Readers are pure: they add their own wrap-immune elapsed time to a snapshot published by `Time::serviceMonotonic()`, which the main loop calls every iteration and which is **the only writer**. Never call `serviceMonotonic()` from anywhere else - two writers can count one wrap twice, putting every uptime and wall-clock reading ~49.7 days into the future for the rest of the boot. Not ISR-safe (the snapshot is read under a seqlock); see the contract in `UptimeClock.h`. Deadline and interval checks should still use `Throttle`, which needs no carry state at all.
|
||||
|
||||
**Sentinel hazard.** If a deadline variable also encodes "inactive" - `0` for `rebootAtMsec`, `shutdownAtMsec`, `alertBannerUntil`, `fixHoldEnds`; `UINT32_MAX` for `nagCycleCutoff` - test that sentinel _before_ the elapsed comparison, and match the test to the sentinel actually in use. `if (deadline && Throttle::deadlinePassed(deadline))` covers the `0` family only; `nagCycleCutoff` needs `deadline != UINT32_MAX`, or a separate armed flag as `ExternalNotificationModule` does with `isNagging`. Every sentinel value is arithmetically far in the past, so a correct comparison reads it as "expired" and fires immediately: `rebootAtMsec = -1` meaning "never" is what would have become a reboot loop. Never fold the sentinel into the helper.
|
||||
|
||||
**And decide which way the sentinel should fall.** "Inactive" does not always mean "suppress". At the GPS fix-hold site `fixHoldEnds == 0` means _no hold is in force_, which is exactly when a new hold must be armed - the naive comparison it replaced was `(fixHoldEnds + GPS_THREAD_INTERVAL) < millis()`, always true when nothing was armed. Guarding it with `fixHoldEnds != 0 &&` looks like this rule and inverts the site: nothing re-arms, nothing publishes, and the receiver stays powered until the search timeout. Read the surrounding logic before adding the guard. `fixHoldInForce()` in `src/gps/GPS.cpp` is the worked example - state the predicate positively, so the sentinel has an honest answer, and derive both decisions from it - with `test/test_gps_fix_hold/` pinning both directions.
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
@@ -663,7 +675,7 @@ Most workflows can be triggered manually via `workflow_dispatch` for testing.
|
||||
|
||||
### Native unit tests (C++)
|
||||
|
||||
Unit tests in `test/` directory. The canonical suite count is in `test/native-suite-count`, cross-checked against `test/test_*` on every full run and by the `suite-count-check` CI job. **Never state the count as a literal anywhere else** - point at that file. The list below is a partial description of what suites cover, not an inventory:
|
||||
Unit tests in `test/` directory. The canonical suite count is detected on the fly: the `test_*` directories under `test/` are the register, and `bin/run-tests.sh` cross-checks the suites that actually ran against them on every full run. **Never state the count as a literal anywhere** - it is whatever `test/test_*` contains right now. In CI, the `suite-shrinkage-check` job (`test_native.yml`) fails a PR that loses a `test_*` directory relative to its merge base unless the suite is named in the PR title, body, or a commit message - deleting a suite therefore requires saying so. The list below is a partial description of what suites cover, not an inventory:
|
||||
|
||||
- `test_admin_radio/` - LoRa region/config validation, AdminModule dispatch, node-DB metadata saves
|
||||
- `test_fscommon_getfiles/` - bounded file-manifest walk (cap, depth, truncation reporting)
|
||||
@@ -693,7 +705,7 @@ Unit tests in `test/` directory. The canonical suite count is in `test/native-su
|
||||
- `test_utf8/` - UTF-8 utilities
|
||||
- `test_warm_store/` - Warm-tier node store
|
||||
|
||||
**Preferred run command - `bin/run-tests.sh`** (defaults to the `coverage` env; emits a machine-readable verdict on the final line; update `test/native-suite-count` when adding or removing suites):
|
||||
**Preferred run command - `bin/run-tests.sh`** (defaults to the `coverage` env; emits a machine-readable verdict on the final line; new `test_*` directories are picked up automatically):
|
||||
|
||||
```bash
|
||||
./bin/run-tests.sh # all suites
|
||||
@@ -712,18 +724,18 @@ Unit tests in `test/` directory. The canonical suite count is in `test/native-su
|
||||
|
||||
Exit codes and verdicts (exact counts will vary; examples below are illustrative):
|
||||
|
||||
| Exit | Verdict | Meaning |
|
||||
| ---- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| 0 | `GREEN` | All canonical suites ran, all passed, no ignored test cases |
|
||||
| 1 | `RED` | At least one failure, build error, or sanitizer fault |
|
||||
| 2 | `AMBER` | All that ran passed, but something was lost or unexplained: a suite silently went missing on a full run, individual test cases were skipped (`TEST_IGNORE`), `test/native-suite-count` disagrees with the `test/` directory count, or a suite left behind shared state it does not declare |
|
||||
| 3 | `FILTERED` | A `-f` run completed cleanly; suites outside the filter were intentionally not run |
|
||||
| Exit | Verdict | Meaning |
|
||||
| ---- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 0 | `GREEN` | All canonical suites ran, all passed, no ignored test cases |
|
||||
| 1 | `RED` | At least one failure, build error, or sanitizer fault |
|
||||
| 2 | `AMBER` | All that ran passed, but something was lost or unexplained: a suite silently went missing on a full run, individual test cases were skipped (`TEST_IGNORE`), or a suite left behind shared state it does not declare |
|
||||
| 3 | `FILTERED` | A `-f` run completed cleanly; suites outside the filter were intentionally not run |
|
||||
|
||||
Examples - exact counts will vary by suite count and env:
|
||||
|
||||
```text
|
||||
# GREEN: all suites ran and passed
|
||||
RESULT: GREEN N/N suites passed [canonical: N/N]
|
||||
RESULT: GREEN N/N suites passed, all CLEAN
|
||||
|
||||
# RED: real test failure
|
||||
RESULT: RED 1 failed
|
||||
@@ -731,14 +743,11 @@ RESULT: RED 1 failed
|
||||
# RED: sanitizer exit-time abort (all tests passed but process aborted at exit)
|
||||
RESULT: RED exit-time abort (tests passed; likely sanitizer - see hint above)
|
||||
|
||||
# AMBER: native-suite-count disagrees with test/ directory count (too low)
|
||||
RESULT: AMBER test/ has 24 suite directories but native-suite-count says 5 - update test/native-suite-count after registering new suites
|
||||
|
||||
# AMBER: native-suite-count disagrees with test/ directory count (too high)
|
||||
RESULT: AMBER test/ has 24 suite directories but native-suite-count says 99 - update test/native-suite-count after removing suites
|
||||
# AMBER: a suite silently went missing on a full run
|
||||
RESULT: AMBER 23/24 suites ran (missing: test_radio) - all that ran passed
|
||||
|
||||
# FILTERED: single suite run completed cleanly
|
||||
RESULT: FILTERED 1/24 suites ran (not run: test_admin_radio test_atak …) - filtered: test_serial [canonical: 1/24]
|
||||
RESULT: FILTERED 1/24 suites ran (not run: test_admin_radio test_atak …) - filtered: test_serial
|
||||
```
|
||||
|
||||
> **Copilot interface note:** When running tests via the Copilot chat interface, edits made through the chat may not be reflected in the on-disk files that the test binary reads. If tests pass in chat but fail locally (or vice versa), verify the files on disk match what you expect before trusting the result. Always confirm with a local terminal run.
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Allowlist for the millis-deadline-check guard in .github/workflows/test_native.yml.
|
||||
#
|
||||
# That guard rejects comparisons made directly against millis(), because they invert while the
|
||||
# deadline sits on the far side of the 32-bit wrap. Use Throttle::deadlinePassed(deadline) or
|
||||
# Throttle::hasElapsed(lastEvent, intervalMs) instead - see .github/copilot-instructions.md.
|
||||
#
|
||||
# Only add a line here when the comparison genuinely is not a deadline test. The usual valid case is
|
||||
# an *uptime threshold*: "has the device been up for at least N ms", where there is no stored
|
||||
# deadline and no event to measure from. Those still misbehave briefly after a wrap - the threshold
|
||||
# is simply re-crossed - which is harmless for boot-holdoff logic and not worth new state.
|
||||
#
|
||||
# Format: <path><TAB><exact trimmed source line, comments stripped>
|
||||
# Line numbers are deliberately absent so edits above an entry do not invalidate it. A `#` comment
|
||||
# on the code line is stripped before matching, so do not include one here.
|
||||
|
||||
# Boot holdoff, not a deadline: suppresses a phantom shutdown from floating pins during the first
|
||||
# 30s of uptime. Pairs with the buttonPressStartTime > 30000 test on the same line.
|
||||
src/input/ButtonThread.cpp if (millis() > 30000 && buttonPressStartTime > 30000 && _longLongPress != INPUT_BROKER_NONE &&
|
||||
|
||||
# Boot-window check, not a deadline: draws the custom OEM logo only during the first 10s of uptime,
|
||||
# so the ordinary Meshtastic logo is used at shutdown.
|
||||
src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp if (millis() < 10 * 1000UL) {
|
||||
@@ -23,13 +23,19 @@ env:
|
||||
LCOV_CAPTURE_FLAGS: --quiet --capture --include "${PWD}/src/*" --exclude '*/src/mesh/generated/*' --directory .pio/build/coverage/src --base-directory "${PWD}"
|
||||
|
||||
jobs:
|
||||
# Guard the registered native-suite total. `platformio test` discovers and runs whatever
|
||||
# test_* directories exist, so it never notices when test/native-suite-count drifts from the
|
||||
# actual directory count (a suite added without registering it, or the file left stale). That
|
||||
# reconciliation only lives in bin/run-tests.sh, which CI does not invoke - so mirror the exact
|
||||
# check here and fail the PR on a mismatch, keeping the manual count honest.
|
||||
suite-count-check:
|
||||
name: Native Suite Count
|
||||
# Tripwire against the native suite set shrinking by accident. `platformio test` discovers and
|
||||
# runs whatever test_* directories exist, and bin/run-tests.sh derives its expected count from
|
||||
# the same walk - so a suite directory lost in a bad rebase or an overzealous cleanup just means
|
||||
# fewer suites run, and every remaining check stays green. Compare the test_* directory list
|
||||
# against the PR's merge base and fail when a suite vanished without the PR saying so: a removed
|
||||
# suite's name must appear in the PR title, the PR body, or a commit message in the PR's range.
|
||||
# A deliberate removal satisfies that by stating what it removes; an accidental loss cannot.
|
||||
# Only pull_request runs have a base to compare against (and PRs are where accidents arrive);
|
||||
# every other event skips. No job depends on this one: a skipped job would skip its dependents,
|
||||
# and the expensive jobs should not wait on a full-history clone.
|
||||
suite-shrinkage-check:
|
||||
name: Native Suite Shrinkage
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-slim
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -37,40 +43,111 @@ jobs:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
# Full history: the merge base must be computed, not guessed from a possibly stale
|
||||
# event payload, and the acknowledgment scan reads every commit message in the range.
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Reconcile native-suite-count with test/ directories
|
||||
- name: Fail if a test_* suite vanished unacknowledged
|
||||
shell: bash
|
||||
# PR title/body are attacker-controlled text; they reach the script through env: only,
|
||||
# never spliced into the shell source (same rule as the suite-order seed below).
|
||||
env:
|
||||
BASE_REF: ${{ github.base_ref }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_BODY: ${{ github.event.pull_request.body }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --quiet origin "$BASE_REF"
|
||||
base=$(git merge-base FETCH_HEAD HEAD)
|
||||
# Same canonical set every other consumer derives: directories named test_* directly
|
||||
# under test/, read from the git trees so the comparison is exact at both endpoints.
|
||||
list_suites() { git ls-tree -d --name-only "$1" test/ | sed 's#^test/##' | grep '^test_' | sort; }
|
||||
removed=$(comm -23 <(list_suites "$base") <(list_suites HEAD))
|
||||
if [[ -z $removed ]]; then
|
||||
echo "No suite removed: $(list_suites HEAD | wc -l) test_* directories, none lost since merge base ${base:0:8}."
|
||||
exit 0
|
||||
fi
|
||||
messages=$(git log --format=%B "$base..HEAD")
|
||||
fail=0
|
||||
while IFS= read -r suite; do
|
||||
if printf '%s\n%s\n%s\n' "$PR_TITLE" "$PR_BODY" "$messages" | grep -qF "$suite"; then
|
||||
echo "Removed suite $suite is named in the PR title/body or a commit message - acknowledged."
|
||||
else
|
||||
echo "::error title=Native suite vanished::test/$suite exists on the merge base but is gone from this PR, and nothing in the PR title, body, or commit messages mentions it. If the removal is deliberate, name $suite in the PR description or a commit message; if not, restore the directory - platformio test would silently run without it."
|
||||
fail=1
|
||||
fi
|
||||
done <<<"$removed"
|
||||
exit $fail
|
||||
|
||||
# Reject naive deadline comparisons against the 32-bit uptime clocks. `millis() > deadline` and
|
||||
# `deadline < millis()` invert while the deadline sits on the far side of the 32-bit wrap: the
|
||||
# action fires immediately, or blocks for about the interval it should have waited. The correct
|
||||
# forms are
|
||||
# Throttle::isWithinTimespanMs / hasElapsed (elapsed since a stored event) and
|
||||
# Throttle::deadlinePassed (an absolute deadline). See .github/copilot-instructions.md.
|
||||
millis-deadline-check:
|
||||
# Name is load-bearing: upstream branch protection matches the check by name. Widen the guard,
|
||||
# not this string.
|
||||
name: Naive millis() Deadline Compare
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Reject 32-bit uptime clocks used directly in a deadline comparison
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
count_file="test/native-suite-count"
|
||||
if [[ ! -f $count_file ]]; then
|
||||
echo "::error title=Missing native-suite-count::$count_file not found - it must record the number of test_* suite directories."
|
||||
exit 1
|
||||
allowlist=".github/millis-deadline-allowlist.txt"
|
||||
|
||||
# Flag millis() or its Time::getMillis() wrapper directly adjacent to a comparison
|
||||
# operator, in either order. The correct idioms subtract first, so they are not matched.
|
||||
#
|
||||
# Line comments are stripped before matching, so prose may name the broken idiom (this
|
||||
# guard's own documentation does). Block comments are not stripped; keep `millis() >` out
|
||||
# of /* */ blocks. mawk-compatible - ubuntu-latest has no gawk.
|
||||
find src -type f \( -name '*.cpp' -o -name '*.h' -o -name '*.hpp' -o -name '*.ino' \) \
|
||||
! -path 'src/mesh/generated/*' -print0 |
|
||||
xargs -0 awk '
|
||||
{
|
||||
line = $0
|
||||
sub(/\/\/.*/, "", line)
|
||||
if (line ~ /((millis|getMillis)\(\)[ \t]*[<>]=?)|([<>]=?[ \t]*(millis|getMillis)\(\))/) {
|
||||
code = line
|
||||
sub(/^[ \t]+/, "", code); sub(/[ \t]+$/, "", code)
|
||||
printf "%s\t%s\t%s\n", FILENAME, FNR, code
|
||||
}
|
||||
}' > /tmp/millis-hits.tsv
|
||||
|
||||
# Allowlisted entries are keyed on file + exact source text, deliberately without a line
|
||||
# number, so unrelated edits above them do not invalidate the entry.
|
||||
: > /tmp/millis-allowed.tsv
|
||||
if [[ -f $allowlist ]]; then
|
||||
grep -vE '^[[:space:]]*(#|$)' "$allowlist" > /tmp/millis-allowed.tsv || true
|
||||
fi
|
||||
# Same canonical set as bin/run-tests.sh: directories named test_* directly under test/.
|
||||
expected_count=$(find test -maxdepth 1 -type d -name 'test_*' -printf '%f\n' | wc -l)
|
||||
canonical_count=$(tr -d '[:space:]' <"$count_file")
|
||||
if ! [[ $canonical_count =~ ^[0-9]+$ ]]; then
|
||||
echo "::error title=Invalid native-suite-count::$count_file must contain a single integer, got '$canonical_count'."
|
||||
exit 1
|
||||
fi
|
||||
echo "test/ directories: $expected_count"
|
||||
echo "native-suite-count: $canonical_count"
|
||||
if [[ $expected_count -ne $canonical_count ]]; then
|
||||
if [[ $expected_count -gt $canonical_count ]]; then
|
||||
hint="a suite was added - bump $count_file to $expected_count"
|
||||
else
|
||||
hint="a suite was removed - lower $count_file to $expected_count"
|
||||
|
||||
violations=0
|
||||
while IFS=$'\t' read -r file line code; do
|
||||
[[ -n ${file:-} ]] || continue
|
||||
if grep -qxF "$(printf '%s\t%s' "$file" "$code")" /tmp/millis-allowed.tsv; then
|
||||
continue
|
||||
fi
|
||||
echo "::error title=native-suite-count mismatch::test/ has $expected_count suite directories but $count_file says $canonical_count ($hint)."
|
||||
echo "$file:$line: $code"
|
||||
violations=$((violations + 1))
|
||||
done < /tmp/millis-hits.tsv
|
||||
|
||||
if [[ $violations -gt 0 ]]; then
|
||||
echo "::error title=Naive uptime deadline compare::$violations line(s) compare a 32-bit uptime clock directly, which inverts while the deadline is on the far side of the 32-bit wrap - the action fires immediately, or blocks for about the interval it should have waited. Use Throttle::deadlinePassed(deadline) for a stored absolute deadline, or Throttle::hasElapsed(lastEvent, intervalMs) for an interval. If a match genuinely is not a deadline test (an uptime threshold, say), add it to $allowlist with a reason."
|
||||
exit 1
|
||||
fi
|
||||
echo "native-suite-count matches the $expected_count suite directories."
|
||||
echo "No naive 32-bit uptime deadline comparisons in src/ (allowlist: $(wc -l < /tmp/millis-allowed.tsv) entr(y/ies))."
|
||||
|
||||
simulator-tests:
|
||||
name: Native Simulator Tests
|
||||
runs-on: ubuntu-24.04-arm
|
||||
needs: suite-count-check
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
@@ -169,7 +246,6 @@ jobs:
|
||||
platformio-tests:
|
||||
name: Native PlatformIO Tests
|
||||
runs-on: ubuntu-24.04-arm
|
||||
needs: suite-count-check
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
@@ -329,13 +405,16 @@ jobs:
|
||||
lcov ${{ env.LCOV_CAPTURE_FLAGS }} --test-name tests --output-file coverage_tests.info
|
||||
sed -i -e "s#${PWD}#.#" coverage_tests.info # Make paths relative.
|
||||
|
||||
- name: Event channel policy tests
|
||||
run: platformio test -e coverage-event-policy -v --junit-output-path event-policy-testreport.xml
|
||||
|
||||
- name: Save test results
|
||||
if: always() # run this step even if previous step failed
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: platformio-test-report-${{ steps.version.outputs.long }}
|
||||
overwrite: true
|
||||
path: ./testreport.xml
|
||||
path: ./*testreport.xml
|
||||
|
||||
- name: Save coverage information
|
||||
uses: actions/upload-artifact@v7
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
name: Update protobufs and regenerate classes
|
||||
on: workflow_dispatch
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
protobufs_branch:
|
||||
description: Branch of meshtastic/protobufs to generate from
|
||||
required: true
|
||||
type: choice
|
||||
default: same-as-this-branch
|
||||
options:
|
||||
- same-as-this-branch
|
||||
- master
|
||||
- develop
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
update-protobufs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions: # Needed for peter-evans/create-pull-request.
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
@@ -14,22 +25,50 @@ jobs:
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: true
|
||||
persist-credentials: false
|
||||
|
||||
- name: Resolve protobufs branch
|
||||
id: resolve
|
||||
env:
|
||||
INPUT_BRANCH: ${{ inputs.protobufs_branch }}
|
||||
TRIGGER_BRANCH: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ "$INPUT_BRANCH" = "same-as-this-branch" ]; then
|
||||
BRANCH="$TRIGGER_BRANCH"
|
||||
else
|
||||
BRANCH="$INPUT_BRANCH"
|
||||
fi
|
||||
case "$BRANCH" in
|
||||
master | develop) ;;
|
||||
*)
|
||||
echo "::error::Refusing to generate from branch '$BRANCH'"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
echo "branch=$BRANCH" >>"$GITHUB_OUTPUT"
|
||||
|
||||
- name: Update submodule
|
||||
if: ${{ github.ref_name == 'master' || github.ref_name == 'develop' }}
|
||||
working-directory: protobufs
|
||||
env:
|
||||
# Use the branch that triggered the workflow as the protobuf branch.
|
||||
GIT_BRANCH: ${{ github.ref_name }}
|
||||
GIT_BRANCH: ${{ steps.resolve.outputs.branch }}
|
||||
run: |
|
||||
git fetch --prune origin $GIT_BRANCH
|
||||
git checkout FETCH_HEAD
|
||||
set -euo pipefail
|
||||
git fetch --prune origin "+refs/heads/${GIT_BRANCH}:refs/remotes/origin/${GIT_BRANCH}"
|
||||
git checkout --detach "refs/remotes/origin/${GIT_BRANCH}"
|
||||
git rev-parse HEAD
|
||||
|
||||
- name: Download nanopb
|
||||
env:
|
||||
NANOPB_VERSION: 0.4.9.1
|
||||
NANOPB_SHA256: 951a9ab2385424a4cdf245d0c84f4c88c6ccbc65a0dade4b246d50c068f24128
|
||||
run: |
|
||||
wget https://github.com/nanopb/nanopb/releases/download/nanopb-0.4.9.1/nanopb-0.4.9.1-linux-x86.tar.gz
|
||||
tar xvzf nanopb-0.4.9.1-linux-x86.tar.gz
|
||||
mv nanopb-0.4.9.1-linux-x86 nanopb-0.4.9
|
||||
set -euo pipefail
|
||||
TARBALL="nanopb-${NANOPB_VERSION}-linux-x86.tar.gz"
|
||||
wget -q "https://github.com/nanopb/nanopb/releases/download/nanopb-${NANOPB_VERSION}/${TARBALL}"
|
||||
echo "${NANOPB_SHA256} ${TARBALL}" | sha256sum -c -
|
||||
tar xzf "${TARBALL}"
|
||||
mv "nanopb-${NANOPB_VERSION}-linux-x86" nanopb-0.4.9
|
||||
|
||||
- name: Re-generate protocol buffers
|
||||
run: |
|
||||
@@ -38,10 +77,12 @@ jobs:
|
||||
- name: Create pull request
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
with:
|
||||
branch: create-pull-request/update-protobufs-${{ github.ref_name }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: create-pull-request/update-protobufs-${{ github.ref_name }}-from-${{ steps.resolve.outputs.branch }}
|
||||
labels: submodules
|
||||
title: Update protobufs and classes
|
||||
commit-message: Update protobufs
|
||||
add-paths: |
|
||||
protobufs
|
||||
src/mesh
|
||||
|
||||
@@ -151,6 +151,15 @@ lint:
|
||||
- linters: [ascii-dash]
|
||||
paths:
|
||||
- src/graphics/fonts/**
|
||||
# millis()-wraparound tests pin dense clusters of hex boundary constants
|
||||
# (0xFFFFFF00u and neighbors). trufflehog's Lob detector stitches nearby
|
||||
# hex literals into one candidate string and the result happens to match
|
||||
# a Lob API key shape. Not secrets - deterministic test fixtures for the
|
||||
# 32-bit rollover.
|
||||
- linters: [trufflehog]
|
||||
paths:
|
||||
- test/test_throttle/test_main.cpp
|
||||
- test/test_uptime_clock/test_main.cpp
|
||||
runtimes:
|
||||
enabled:
|
||||
- python@3.14.4
|
||||
|
||||
@@ -81,7 +81,18 @@ Key rotation to never trigger casually: only the **full** factory reset (`factor
|
||||
- **Never edit or commit files under `src/mesh/generated/`.** They are regenerated from the [`meshtastic/protobufs`](https://github.com/meshtastic/protobufs) repo by the `update_protobufs.yml` workflow (entry point: `bin/regen-protos.sh`). Local edits will be overwritten and create merge conflicts. If a `.proto` change is needed, open a PR against the protobufs repo first, then let the workflow re-sync this repo.
|
||||
- **`confirm=True` on destructive MCP tools is a real gate, not a formality.** Don't bypass it via auto-approve settings.
|
||||
- **Keep code comments minimal - one or two lines, max.** Comment only when the _why_ isn't obvious from the code; never restate what the next line does. No multi-paragraph block comments explaining straightforward changes. The diff and commit message carry the rationale; the code carries the behavior.
|
||||
- **Use `Throttle` for time-based rate limiting, not raw `millis()` math.** `src/mesh/Throttle.h` provides `Throttle::isWithinTimespanMs(lastMs, intervalMs)` (returns true while inside the cooldown) and `Throttle::execute(&lastMs, intervalMs, func)` (function-pointer form that updates the timestamp on fire). Use these for any "did N ms pass since X" check - raw `millis() > lastMs + N` is rollover-unsafe (breaks after ~49.7 days) and inconsistent with the rest of the codebase. The helpers compute `now - lastMs` with unsigned subtraction, which wraps correctly.
|
||||
- **Never compare against `millis()` directly. Use `Throttle`.** `src/mesh/Throttle.h` is the sanctioned way to ask about time, and CI enforces this (`millis-deadline-check` in `.github/workflows/test_native.yml` fails the PR on a new `millis() >` / `< millis()` comparison).
|
||||
- `Throttle::isWithinTimespanMs(lastMs, intervalMs)` - true while still inside the cooldown.
|
||||
- `Throttle::hasElapsed(lastMs, intervalMs)` - its complement, true once the interval has passed (inclusive `>=`). Prefer this to spelling `!isWithinTimespanMs(...)`.
|
||||
- `Throttle::execute(&lastMs, intervalMs, func)` - function-pointer form that updates the timestamp on fire.
|
||||
- `Throttle::deadlinePassed(deadlineMs)` - for a stored absolute deadline that cannot be re-expressed as "interval since an event".
|
||||
- `Throttle::deadlinePassedAt(nowMs, deadlineMs)` - the same test against a caller-supplied `now`, for a loop that snapshots the clock once and tests many deadlines. Snapshot from `Time::getMillis()`.
|
||||
|
||||
Raw `millis() > deadline` or `deadline < millis()` is rollover-unsafe: the comparison inverts while the deadline sits on the far side of the 32-bit wrap, so the action fires immediately or blocks for roughly the interval it should have waited. All five helpers subtract first, so unsigned wraparound cancels out. `Throttle` reads the clock through `Time::getMillis()` (`src/UptimeClock.h`), so all ~94 of its call sites are time-injectable and a native test can drive the wrap with `Time::setTestMillis()`.
|
||||
|
||||
**Sentinel hazard.** If a deadline variable also encodes "inactive" (`0` for `rebootAtMsec`, `shutdownAtMsec`, `alertBannerUntil`, `fixHoldEnds`; `UINT32_MAX` for `nagCycleCutoff`), test that sentinel _before_ the elapsed comparison - every such value is arithmetically far in the past, so a correct comparison fires on it immediately. Match the test to the sentinel in use: `if (deadline && Throttle::deadlinePassed(deadline))` covers the `0` family, `nagCycleCutoff` needs `deadline != UINT32_MAX` or a separate armed flag (`isNagging`).
|
||||
|
||||
Then decide which way the sentinel should fall - "inactive" does not always mean "suppress". At the GPS fix-hold site `fixHoldEnds == 0` means _no hold is in force_, which is exactly when one must be armed; guarding it with `fixHoldEnds != 0 &&` looks like this rule and inverts the site. See `fixHoldInForce()` in `src/gps/GPS.cpp` and `test/test_gps_fix_hold/`.
|
||||
|
||||
## Typical agent workflows
|
||||
|
||||
@@ -131,7 +142,7 @@ Sequence these; don't parallelize on the same port.
|
||||
| `src/modules/` | Feature modules; `Telemetry/Sensor/` has 50+ I2C sensor drivers |
|
||||
| `variants/` | 200+ hardware variant definitions (`variant.h` + `platformio.ini` per board) |
|
||||
| `protobufs/` | `.proto` definitions; regenerate with `bin/regen-protos.sh` |
|
||||
| `test/` | Firmware unit tests (count: `test/native-suite-count`; `./bin/run-tests.sh` preferred, falls back to `pio test -e native`) |
|
||||
| `test/` | Firmware unit tests (count = the `test_*` dirs, detected on the fly; `./bin/run-tests.sh` preferred, falls back to `pio test -e native`) |
|
||||
| [meshtastic-mcp](https://github.com/meshtastic/meshtastic-mcp) | Standalone MCP server + tiered pytest hardware harness (`unit/`, `mesh/`, `telemetry/`, `monitor/`, `recovery/`, `ui/`, `fleet/`, `admin/`, `provisioning/`) - registered here via `.mcp.json` |
|
||||
| `.github/prompts/` | Copilot prompt bodies (firmware scaffolding: new module / sensor / variant) |
|
||||
| `.github/copilot-instructions.md` | **Primary agent instructions - read this** |
|
||||
|
||||
@@ -11,12 +11,13 @@
|
||||
>
|
||||
> **Need this? It's here.**
|
||||
>
|
||||
> | | |
|
||||
> | ------------------------------------------- | ---------------------------------------------------------- |
|
||||
> | General helpers (clamp, UTF-8, string fmt…) | `src/meshUtils.h` |
|
||||
> | Logging macros (LOG_DEBUG / INFO / WARN…) | `src/DebugConfiguration.h` |
|
||||
> | New module skeleton | inherit `ProtobufModule<T>` in `src/mesh/ProtobufModule.h` |
|
||||
> | Observer / event wiring | `src/Observer.h` |
|
||||
> | | |
|
||||
> | --------------------------------------------------------- | ---------------------------------------------------------- |
|
||||
> | General helpers (clamp, UTF-8, string fmt…) | `src/meshUtils.h` |
|
||||
> | Logging macros (LOG_DEBUG / INFO / WARN…) | `src/DebugConfiguration.h` |
|
||||
> | Elapsed time / deadlines (never bare `millis()` compares) | `src/mesh/Throttle.h` |
|
||||
> | New module skeleton | inherit `ProtobufModule<T>` in `src/mesh/ProtobufModule.h` |
|
||||
> | Observer / event wiring | `src/Observer.h` |
|
||||
|
||||
**Read `.github/copilot-instructions.md` first.** That file is the canonical agent-facing document for this repo. It covers project layout, coding conventions, the build system, CI/CD, the native C++ test suite, and the MCP Server & Hardware Test Harness. Read it top-to-bottom before starting any non-trivial change.
|
||||
|
||||
|
||||
+30
-11
@@ -70,7 +70,7 @@ IF "__!FILENAME!__"=="____" (
|
||||
CALL :LOG_MESSAGE ERROR "Filename containing spaces are not supported."
|
||||
GOTO help
|
||||
)
|
||||
IF NOT "__!FILENAME:.factory.bin=!__"=="__!FILENAME!__" (
|
||||
IF /I NOT "!FILENAME:~-12!"==".factory.bin" (
|
||||
CALL :LOG_MESSAGE ERROR "Filename must be a firmware-*.factory.bin file."
|
||||
GOTO help
|
||||
)
|
||||
@@ -111,7 +111,7 @@ IF EXIST !METAFILE! (
|
||||
|
||||
CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..."
|
||||
IF NOT "__%PYTHON%__"=="____" (
|
||||
SET "ESPTOOL_CMD=!PYTHON! -m esptool"
|
||||
SET "ESPTOOL_CMD="!PYTHON!" -m esptool"
|
||||
CALL :LOG_MESSAGE DEBUG "Python interpreter supplied."
|
||||
) ELSE (
|
||||
CALL :LOG_MESSAGE DEBUG "Python interpreter NOT supplied. Looking for esptool..."
|
||||
@@ -126,12 +126,31 @@ IF NOT "__%PYTHON%__"=="____" (
|
||||
)
|
||||
|
||||
CALL :LOG_MESSAGE DEBUG "Checking esptool command !ESPTOOL_CMD!..."
|
||||
!ESPTOOL_CMD! >nul 2>&1
|
||||
IF %ERRORLEVEL% EQU 9009 (
|
||||
@REM 9009 = command not found on Windows
|
||||
@REM %VAR% not !VAR!: cmd will not split a delayed-expanded command token that
|
||||
@REM carries a path, so the "python -m esptool" form never starts.
|
||||
%ESPTOOL_CMD% >nul 2>&1
|
||||
SET "ESPTOOL_EXIT=!ERRORLEVEL!"
|
||||
@REM 9009 = command not found, 3 = bad path from -P. Both mean unusable.
|
||||
IF !ESPTOOL_EXIT! EQU 3 SET "ESPTOOL_EXIT=9009"
|
||||
IF !ESPTOOL_EXIT! EQU 9009 (
|
||||
CALL :LOG_MESSAGE ERROR "esptool not found: !ESPTOOL_CMD!"
|
||||
EXIT /B 1
|
||||
)
|
||||
|
||||
@REM esptool v5 renamed subcommands to dashes; older versions only take underscores.
|
||||
@REM Probe here: the --debug and --port rewrites below leave ESPTOOL_CMD unusable.
|
||||
SET "ESPTOOL_WRITE_FLASH=write_flash"
|
||||
SET "ESPTOOL_ERASE_FLASH=erase_flash"
|
||||
SET "ESPTOOL_READ_FLASH_STATUS=read_flash_status"
|
||||
%ESPTOOL_CMD% 2>&1 | findstr /C:"write-flash" >nul
|
||||
IF !ERRORLEVEL! EQU 0 (
|
||||
SET "ESPTOOL_WRITE_FLASH=write-flash"
|
||||
SET "ESPTOOL_ERASE_FLASH=erase-flash"
|
||||
SET "ESPTOOL_READ_FLASH_STATUS=read-flash-status"
|
||||
)
|
||||
CALL :RESET_ERROR
|
||||
CALL :LOG_MESSAGE DEBUG "Using esptool write command: !ESPTOOL_WRITE_FLASH!"
|
||||
|
||||
IF %DEBUG% EQU 1 (
|
||||
CALL :LOG_MESSAGE DEBUG "Skipping ESPTOOL_CMD steps."
|
||||
SET "ESPTOOL_CMD=REM !ESPTOOL_CMD!"
|
||||
@@ -148,7 +167,7 @@ CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!."
|
||||
|
||||
IF %BPS_RESET% EQU 1 (
|
||||
@REM Attempt to change mode via 1200bps Reset.
|
||||
CALL :RUN_ESPTOOL 1200 --after no_reset read_flash_status
|
||||
CALL :RUN_ESPTOOL 1200 --after no_reset !ESPTOOL_READ_FLASH_STATUS!
|
||||
GOTO eof
|
||||
)
|
||||
|
||||
@@ -174,14 +193,14 @@ IF NOT EXIST !SPIFFS_FILENAME! CALL :LOG_MESSAGE ERROR "File does not exist: "!S
|
||||
|
||||
@REM Flashing operations.
|
||||
CALL :LOG_MESSAGE INFO "Trying to flash "!FILENAME!", but first erasing and writing system information..."
|
||||
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! erase_flash || GOTO eof
|
||||
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash 0x00 "!FILENAME!" || GOTO eof
|
||||
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! !ESPTOOL_ERASE_FLASH! || GOTO eof
|
||||
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! !ESPTOOL_WRITE_FLASH! 0x00 "!FILENAME!" || GOTO eof
|
||||
|
||||
CALL :LOG_MESSAGE INFO "Trying to flash BLEOTA "!OTA_FILENAME!" at OTA_OFFSET !OTA_OFFSET!..."
|
||||
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash !OTA_OFFSET! "!OTA_FILENAME!" || GOTO eof
|
||||
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! !ESPTOOL_WRITE_FLASH! !OTA_OFFSET! "!OTA_FILENAME!" || GOTO eof
|
||||
|
||||
CALL :LOG_MESSAGE INFO "Trying to flash SPIFFS "!SPIFFS_FILENAME!" at SPIFFS_OFFSET !SPIFFS_OFFSET!..."
|
||||
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash !SPIFFS_OFFSET! "!SPIFFS_FILENAME!" || GOTO eof
|
||||
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! !ESPTOOL_WRITE_FLASH! !SPIFFS_OFFSET! "!SPIFFS_FILENAME!" || GOTO eof
|
||||
|
||||
CALL :LOG_MESSAGE INFO "Script complete!."
|
||||
|
||||
@@ -198,7 +217,7 @@ EXIT /B %ERRORLEVEL%
|
||||
@REM Example:: CALL :RUN_ESPTOOL 115200 write_flash 0x10000 "firmwarefile.bin"
|
||||
IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4"
|
||||
CALL :RESET_ERROR
|
||||
!ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4
|
||||
%ESPTOOL_CMD% --baud %~1 %~2 %~3 %~4
|
||||
IF %BPS_RESET% EQU 1 GOTO :eof
|
||||
IF %ERRORLEVEL% NEQ 0 (
|
||||
CALL :LOG_MESSAGE ERROR "Error running command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4"
|
||||
|
||||
+27
-8
@@ -90,7 +90,7 @@ IF NOT "__!FILENAME:.factory.bin=!__"=="__!FILENAME!__" (
|
||||
|
||||
CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..."
|
||||
IF NOT "__%PYTHON%__"=="____" (
|
||||
SET "ESPTOOL_CMD=""!PYTHON!"" -m esptool"
|
||||
SET "ESPTOOL_CMD="!PYTHON!" -m esptool"
|
||||
CALL :LOG_MESSAGE DEBUG "Python interpreter supplied."
|
||||
) ELSE (
|
||||
CALL :LOG_MESSAGE DEBUG "Python interpreter NOT supplied. Looking for esptool..."
|
||||
@@ -105,13 +105,32 @@ IF NOT "__%PYTHON%__"=="____" (
|
||||
)
|
||||
|
||||
CALL :LOG_MESSAGE DEBUG "Checking esptool command !ESPTOOL_CMD!..."
|
||||
!ESPTOOL_CMD! >nul 2>&1
|
||||
CALL :LOG_MESSAGE DEBUG "esptool exit code: %ERRORLEVEL%"
|
||||
IF %ERRORLEVEL% EQU 9009 (
|
||||
@REM 9009 = command not found on Windows
|
||||
@REM %VAR% not !VAR!: cmd will not split a delayed-expanded command token that
|
||||
@REM carries a path, so the "python -m esptool" form never starts.
|
||||
%ESPTOOL_CMD% >nul 2>&1
|
||||
SET "ESPTOOL_EXIT=!ERRORLEVEL!"
|
||||
CALL :LOG_MESSAGE DEBUG "esptool exit code: !ESPTOOL_EXIT!"
|
||||
@REM 9009 = command not found, 3 = bad path from -P. Both mean unusable.
|
||||
IF !ESPTOOL_EXIT! EQU 3 SET "ESPTOOL_EXIT=9009"
|
||||
IF !ESPTOOL_EXIT! EQU 9009 (
|
||||
CALL :LOG_MESSAGE ERROR "esptool not found: !ESPTOOL_CMD!"
|
||||
EXIT /B 1
|
||||
)
|
||||
|
||||
@REM esptool v5 renamed subcommands to dashes; older versions only take underscores.
|
||||
@REM Probe here: the --debug and --port rewrites below leave ESPTOOL_CMD unusable.
|
||||
SET "ESPTOOL_WRITE_FLASH=write_flash"
|
||||
SET "ESPTOOL_ERASE_FLASH=erase_flash"
|
||||
SET "ESPTOOL_READ_FLASH_STATUS=read_flash_status"
|
||||
%ESPTOOL_CMD% 2>&1 | findstr /C:"write-flash" >nul
|
||||
IF !ERRORLEVEL! EQU 0 (
|
||||
SET "ESPTOOL_WRITE_FLASH=write-flash"
|
||||
SET "ESPTOOL_ERASE_FLASH=erase-flash"
|
||||
SET "ESPTOOL_READ_FLASH_STATUS=read-flash-status"
|
||||
)
|
||||
CALL :RESET_ERROR
|
||||
CALL :LOG_MESSAGE DEBUG "Using esptool write command: !ESPTOOL_WRITE_FLASH!"
|
||||
|
||||
IF %DEBUG% EQU 1 (
|
||||
CALL :LOG_MESSAGE DEBUG "Skipping ESPTOOL_CMD steps."
|
||||
SET "ESPTOOL_CMD=REM !ESPTOOL_CMD!"
|
||||
@@ -128,13 +147,13 @@ CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!."
|
||||
|
||||
IF %CHANGE_MODE% EQU 1 (
|
||||
@REM Attempt to change mode via 1200bps Reset.
|
||||
CALL :RUN_ESPTOOL !RESET_BAUD! --after no_reset read_flash_status
|
||||
CALL :RUN_ESPTOOL !RESET_BAUD! --after no_reset !ESPTOOL_READ_FLASH_STATUS!
|
||||
GOTO eof
|
||||
)
|
||||
|
||||
@REM Flashing operations.
|
||||
CALL :LOG_MESSAGE INFO "Trying to flash update "!FILENAME!" at OFFSET !UPDATE_OFFSET!..."
|
||||
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write-flash !UPDATE_OFFSET! "!FILENAME!" || GOTO eof
|
||||
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! !ESPTOOL_WRITE_FLASH! !UPDATE_OFFSET! "!FILENAME!" || GOTO eof
|
||||
|
||||
CALL :LOG_MESSAGE INFO "Script complete!."
|
||||
|
||||
@@ -151,7 +170,7 @@ EXIT /B %ERRORLEVEL%
|
||||
@REM Example:: CALL :RUN_ESPTOOL 115200 write-flash 0x10000 "firmwarefile.bin"
|
||||
IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4"
|
||||
CALL :RESET_ERROR
|
||||
!ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4
|
||||
%ESPTOOL_CMD% --baud %~1 %~2 %~3 %~4
|
||||
IF %CHANGE_MODE% EQU 1 GOTO :eof
|
||||
IF %ERRORLEVEL% NEQ 0 (
|
||||
CALL :LOG_MESSAGE ERROR "Error running command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4"
|
||||
|
||||
+13
-38
@@ -163,20 +163,11 @@ export MESHTASTIC_TEST_STATE_SUMMARY="$STATE_SUMMARY"
|
||||
$KEEP_STATE && export MESHTASTIC_TEST_KEEP_STATE=1
|
||||
$WRITE_MANIFEST && export MESHTASTIC_TEST_KEEP_STATE=1
|
||||
|
||||
# Canonical suite set = the directories in test/. This is the source of truth for
|
||||
# "what should run"; a filtered run only expects its filtered suite.
|
||||
# Canonical suite set = the directories in test/, detected on the fly. This is the sole source
|
||||
# of truth for "what should run"; a filtered run only expects its filtered suite.
|
||||
mapfile -t ALL_SUITES < <(find test -maxdepth 1 -type d -name 'test_*' -printf '%f\n' | sort)
|
||||
EXPECTED_COUNT=${#ALL_SUITES[@]}
|
||||
|
||||
# Canonical suite count - the registered total, maintained in test/native-suite-count.
|
||||
# Update that file whenever a test suite is added or removed.
|
||||
CANONICAL_COUNT_FILE="test/native-suite-count"
|
||||
if [[ -f $CANONICAL_COUNT_FILE ]]; then
|
||||
CANONICAL_COUNT=$(tr -d '[:space:]' <"$CANONICAL_COUNT_FILE")
|
||||
else
|
||||
CANONICAL_COUNT=""
|
||||
fi
|
||||
|
||||
# Cached object-count for this env, written after each completed build (in the gitignored build
|
||||
# dir). Used as the progress denominator: accurate for a full rebuild (every object recompiles),
|
||||
# only a rough upper bound for an incremental run.
|
||||
@@ -462,31 +453,15 @@ if ! grep -qE "$PASS_RE" "$LOG"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Canonical-count rating suffix - appended to every verdict line so the result is always
|
||||
# rated against the registered total, not just the directory count.
|
||||
# If the two counts diverge (suite added/removed without updating native-suite-count), that
|
||||
# is itself surfaced as AMBER before we reach any verdict.
|
||||
canonical_rating() {
|
||||
# Verdict-line suffix. The suite count itself is derived from the test_* directories on the fly
|
||||
# (EXPECTED_COUNT above), so the only extra context a verdict needs is the shuffle seed - carried
|
||||
# into the machine-readable line so a verdict is always replayable from it alone.
|
||||
verdict_suffix() {
|
||||
local rating=""
|
||||
if [[ -n $CANONICAL_COUNT ]]; then
|
||||
rating="[canonical: ${RAN_COUNT}/${CANONICAL_COUNT}]"
|
||||
fi
|
||||
# Carry the seed into the machine-readable line so a verdict is always replayable from it alone.
|
||||
$SHUFFLE && rating="$rating [seed: $SEED]"
|
||||
$SHUFFLE && rating="[seed: $SEED]"
|
||||
echo "$rating"
|
||||
}
|
||||
|
||||
# AMBER: directory count disagrees with native-suite-count - file needs updating.
|
||||
if [[ -n $CANONICAL_COUNT && $EXPECTED_COUNT -ne $CANONICAL_COUNT ]]; then
|
||||
echo ""
|
||||
if [[ $EXPECTED_COUNT -gt $CANONICAL_COUNT ]]; then
|
||||
echo "RESULT: AMBER test/ has $EXPECTED_COUNT suite directories but native-suite-count says $CANONICAL_COUNT - update test/native-suite-count after registering new suites"
|
||||
else
|
||||
echo "RESULT: AMBER test/ has $EXPECTED_COUNT suite directories but native-suite-count says $CANONICAL_COUNT - update test/native-suite-count after removing suites"
|
||||
fi
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# --- Shared-state axis --------------------------------------------------------
|
||||
# Read what the per-suite wrapper recorded. Reported after the count checks so a structural problem
|
||||
# still wins, and before the pass/fail verdict lines so the state summary always prints.
|
||||
@@ -546,7 +521,7 @@ if [[ $IGNORED_COUNT -gt 0 ]]; then
|
||||
echo ""
|
||||
echo "$IGNORE_DETAIL"
|
||||
echo ""
|
||||
echo "RESULT: AMBER ${IGNORED_COUNT} test case(s) ignored $(canonical_rating)"
|
||||
echo "RESULT: AMBER ${IGNORED_COUNT} test case(s) ignored $(verdict_suffix)"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
@@ -558,7 +533,7 @@ if [[ -z $FILTER && $ACCOUNTED_COUNT -lt $EXPECTED_COUNT ]]; then
|
||||
printf '%s\n' "${RAN_SUITES[@]}" "${SKIPPED_SUITES[@]}" | grep -qx "$s" || missing+=("$s")
|
||||
done
|
||||
echo ""
|
||||
echo "RESULT: AMBER ${RAN_COUNT}/${EXPECTED_COUNT} suites ran (missing: ${missing[*]}) - all that ran passed $(canonical_rating)"
|
||||
echo "RESULT: AMBER ${RAN_COUNT}/${EXPECTED_COUNT} suites ran (missing: ${missing[*]}) - all that ran passed $(verdict_suffix)"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
@@ -572,7 +547,7 @@ if ((${#DIRTY_SUITES[@]} > 0)); then
|
||||
echo ""
|
||||
echo " -> declare these in test/state-manifest.tsv with a reason, or stop the write."
|
||||
echo " -> ./bin/run-tests.sh --write-manifest prints the entries to paste."
|
||||
echo "RESULT: AMBER ${#DIRTY_SUITES[@]} suite(s) left undeclared shared state $(canonical_rating)"
|
||||
echo "RESULT: AMBER ${#DIRTY_SUITES[@]} suite(s) left undeclared shared state $(verdict_suffix)"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
@@ -588,7 +563,7 @@ if ((${#SURVIVOR_SUITES[@]} > 0)); then
|
||||
echo ""
|
||||
echo " -> end every setup() branch with exit(UNITY_END()), not a bare UNITY_END()."
|
||||
echo " -> ./bin/lint-unity-exit.sh test/**/*.cpp finds the sites; see test/README.md."
|
||||
echo "RESULT: AMBER ${#SURVIVOR_SUITES[@]} suite(s) still running after the suite finished $(canonical_rating)"
|
||||
echo "RESULT: AMBER ${#SURVIVOR_SUITES[@]} suite(s) still running after the suite finished $(verdict_suffix)"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
@@ -599,10 +574,10 @@ if [[ -n $FILTER ]]; then
|
||||
for s in "${ALL_SUITES[@]}"; do
|
||||
printf '%s\n' "${RAN_SUITES[@]}" "${SKIPPED_SUITES[@]}" | grep -qx "$s" || not_run+=("$s")
|
||||
done
|
||||
echo "RESULT: FILTERED ${RAN_COUNT}/${EXPECTED_COUNT} suites ran (not run: ${not_run[*]}) - filtered: $FILTER $(canonical_rating)"
|
||||
echo "RESULT: FILTERED ${RAN_COUNT}/${EXPECTED_COUNT} suites ran (not run: ${not_run[*]}) - filtered: $FILTER $(verdict_suffix)"
|
||||
exit 3
|
||||
fi
|
||||
|
||||
# GREEN: all canonical suites ran, all passed, no ignored test cases, nothing undeclared left behind.
|
||||
echo "RESULT: GREEN ${RAN_COUNT}/${EXPECTED_COUNT} suites passed, all CLEAN $(canonical_rating)"
|
||||
echo "RESULT: GREEN ${RAN_COUNT}/${EXPECTED_COUNT} suites passed, all CLEAN $(verdict_suffix)"
|
||||
exit 0
|
||||
@@ -12,8 +12,9 @@
|
||||
# anything" is what catches a rule that reports the right number of findings in the wrong places, or
|
||||
# that collapses two findings on one line into one.
|
||||
#
|
||||
# Not a Unity suite and not counted in test/native-suite-count - same arrangement as
|
||||
# bin/test-state-check.sh, and for the same reason: it asserts the behaviour of a process.
|
||||
# Not a Unity suite and not a test_* directory, so outside the suite count run-tests.sh derives
|
||||
# from test/ - same arrangement as bin/test-state-check.sh, and for the same reason: it asserts
|
||||
# the behaviour of a process.
|
||||
#
|
||||
# Usage: ./bin/test-lint-unity-exit.sh (exit 0 = all fixtures behaved)
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
# before-empty assertion fires, because an after-diff measured against a dirty baseline reports
|
||||
# green while meaning nothing.
|
||||
#
|
||||
# Not a Unity suite and not counted in test/native-suite-count - the same arrangement as
|
||||
# bin/test-config-check.sh, and for the same reason: what it asserts is the behaviour of a process,
|
||||
# not of a linkable function.
|
||||
# Not a Unity suite and not a test_* directory, so outside the suite count run-tests.sh derives
|
||||
# from test/ - the same arrangement as bin/test-config-check.sh, and for the same reason: what it
|
||||
# asserts is the behaviour of a process, not of a linkable function.
|
||||
#
|
||||
# Usage: ./bin/test-state-check.sh (exit 0 = all fixtures behaved)
|
||||
|
||||
|
||||
@@ -233,9 +233,26 @@ def _assert_isr_handlers_survived(source, target, env):
|
||||
# and the build stays green. Turn that into a red build:
|
||||
# 1. the linked variant.cpp.o must not be an LTO object (proves the -fno-lto recompile fired);
|
||||
# 2. any override the object defines strong must resolve strong in the ELF.
|
||||
#
|
||||
# The list must also cover Meshtastic's OWN weak variant hooks, not just the core's. Those have
|
||||
# a second, independent way to vanish: their weak default AND their call site sit in the same
|
||||
# LTO'd translation unit (src/main.cpp, src/platform/nrf52/main-nrf52.cpp), so GCC inlines the
|
||||
# empty body at the call site and never reaches for the variant's strong override -- the
|
||||
# -fno-lto middleware above cannot help, the caller is the problem. The definitions carry
|
||||
# __attribute__((noinline)) to prevent it; this guard is what catches a future one that forgets.
|
||||
# Regression that motivated the extension: 2.8 dropped earlyInitVariant() on the muzi R1 Neo, so
|
||||
# DCDC_EN_HOLD/NRF_ON were never driven and the IO controller read the nRF as stuck in DFU
|
||||
# (purple LED). The build stayed green because only _Z11initVariantv was listed here.
|
||||
_VARIANT_OVERRIDES = (
|
||||
"_Z11initVariantv",
|
||||
) # extend if the core grows more weak variant hooks
|
||||
"_Z11initVariantv", # core hook (cores/nRF5/main.cpp)
|
||||
"_Z16earlyInitVariantv", # src/main.cpp -- pre-peripheral board bring-up
|
||||
"_Z15lateInitVariantv", # src/main.cpp -- post-radio board bring-up
|
||||
"_Z16variant_shutdownv", # main-nrf52.cpp -- pin parking before System OFF
|
||||
"_Z21variant_nrf52LoopHookv", # main-nrf52.cpp -- per-loop variant hook
|
||||
"_Z31variant_enableBatteryLpcompWakev", # main-nrf52.cpp -- LPCOMP wake opt-out
|
||||
"_Z20variantDefaultConfigv", # NodeDB.cpp -- per-board config defaults
|
||||
"_Z26variantDefaultModuleConfigv", # NodeDB.cpp -- per-board module defaults
|
||||
) # extend if the core (or Meshtastic) grows more weak variant hooks
|
||||
|
||||
|
||||
def _assert_variant_survived(source, target, env):
|
||||
@@ -289,15 +306,19 @@ def _assert_variant_survived(source, target, env):
|
||||
):
|
||||
problems.append(
|
||||
"%s is strong in variant.cpp.o but weak/absent in the ELF "
|
||||
"(LTO resolved the core's call to the empty weak stub)" % sym
|
||||
"(LTO resolved the call to the empty weak stub)" % sym
|
||||
)
|
||||
if problems:
|
||||
sys.stderr.write(
|
||||
"\n*** nrf52 LTO guard: board variant DROPPED from the image ***\n%s\n"
|
||||
"The variant's early hardware setup (initVariant) will never run on this board.\n"
|
||||
"Check _is_board_variant() in extra_scripts/nrf52_lto.py -- middleware nodes are\n"
|
||||
"$BUILD_DIR-mirrored; match srcnode() paths, not node.get_abspath().\n\n"
|
||||
% "\n".join(" - " + p for p in problems)
|
||||
"\n*** nrf52 LTO guard: board variant override DROPPED from the image ***\n%s\n"
|
||||
"That board hardware setup silently will not run. Two possible causes:\n"
|
||||
" 1. The weak default and its CALL SITE share one LTO'd translation unit\n"
|
||||
" (src/main.cpp, src/platform/nrf52/main-nrf52.cpp, src/mesh/NodeDB.cpp), so GCC\n"
|
||||
" inlined the empty body and never reached the override. Fix: mark BOTH the weak\n"
|
||||
" declaration and definition __attribute__((noinline)) -- see earlyInitVariant().\n"
|
||||
" 2. The -fno-lto middleware stopped matching the variant. Check _is_board_variant()\n"
|
||||
" below -- middleware nodes are $BUILD_DIR-mirrored, so match srcnode() paths,\n"
|
||||
" not node.get_abspath().\n\n" % "\n".join(" - " + p for p in problems)
|
||||
)
|
||||
from SCons.Script import Exit
|
||||
|
||||
|
||||
+12
-2
@@ -137,7 +137,7 @@ lib_deps =
|
||||
[device-ui_base]
|
||||
lib_deps =
|
||||
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
|
||||
https://github.com/meshtastic/device-ui/archive/d1edf7144e902367e57aa7adbfaf2fa12b4d2034.zip
|
||||
https://github.com/meshtastic/device-ui/archive/6a52e33ad81e9b1d060a6db52b36c9535c742b45.zip
|
||||
custom_sdkconfig =
|
||||
# CONFIG_MBEDTLS_INTERNAL_MEM_ALLOC is not set
|
||||
CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC=y
|
||||
@@ -247,8 +247,18 @@ lib_deps =
|
||||
closedcube/ClosedCube OPT3001@1.1.2
|
||||
# renovate: datasource=git-refs depName=meshtastic-DFRobot_LarkWeatherStation packageName=https://github.com/meshtastic/DFRobot_LarkWeatherStation gitBranch=master
|
||||
https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip
|
||||
# renovate: datasource=github-tags depName=Sensirion Core packageName=sensirion/arduino-core
|
||||
https://github.com/Sensirion/arduino-core/archive/refs/tags/0.7.3.zip
|
||||
# renovate: datasource=github-tags depName=Sensirion I2C SCD4x packageName=sensirion/arduino-i2c-scd4x
|
||||
https://github.com/Sensirion/arduino-i2c-scd4x/archive/refs/tags/1.1.0.zip
|
||||
# renovate: datasource=github-tags depName=Sensirion I2C SFA3x packageName=sensirion/arduino-i2c-sfa3x
|
||||
https://github.com/Sensirion/arduino-i2c-sfa3x/archive/refs/tags/1.0.0.zip
|
||||
# renovate: datasource=github-tags depName=Sensirion I2C SCD30 packageName=sensirion/arduino-i2c-scd30
|
||||
https://github.com/Sensirion/arduino-i2c-scd30/archive/1.1.1.zip
|
||||
# renovate: datasource=github-tags depName=arduino-sht packageName=sensirion/arduino-sht
|
||||
https://github.com/Sensirion/arduino-sht/archive/refs/tags/v1.2.6.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit DS248x packageName=adafruit/Adafruit_DS248x
|
||||
https://github.com/adafruit/Adafruit_DS248x/archive/refs/tags/1.2.0.zip
|
||||
https://github.com/adafruit/Adafruit_DS248x/archive/refs/tags/1.2.0.zip
|
||||
|
||||
; Environmental sensors with BSEC2 (Bosch proprietary IAQ)
|
||||
[environmental_extra]
|
||||
|
||||
+1
-1
Submodule protobufs updated: cd290ba246...84bfb0fdb3.
@@ -66,7 +66,7 @@ class AmbientLightingThread : public concurrency::OSThread
|
||||
#if defined(HAS_NCP5623) || defined(HAS_LP5562)
|
||||
_type = type;
|
||||
if (_type == ScanI2C::DeviceType::NONE) {
|
||||
LOG_DEBUG("AmbientLighting Disable due to no RGB leds found on I2C bus");
|
||||
LOG_DEBUG("AmbientLighting disabled: no RGB leds on I2C");
|
||||
disable();
|
||||
return;
|
||||
}
|
||||
@@ -92,7 +92,7 @@ class AmbientLightingThread : public concurrency::OSThread
|
||||
pixels.setBrightness(moduleConfig.ambient_lighting.current);
|
||||
#endif
|
||||
if (!moduleConfig.ambient_lighting.led_state) {
|
||||
LOG_DEBUG("AmbientLighting Disable due to moduleConfig.ambient_lighting.led_state OFF");
|
||||
LOG_DEBUG("AmbientLighting disabled: led_state OFF");
|
||||
disable();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,16 @@ extern MemGet memGet;
|
||||
|
||||
#define DEBUG_PORT (*console) // Serial debug port
|
||||
|
||||
// LOG_TRACE costs no flash unless enabled: -DMESHTASTIC_TRACE_LOGGING(=1) turns it on, =0 forces it off.
|
||||
// Default is on only for portduino (traceFilename packet traces, logoutputlevel=trace), off elsewhere.
|
||||
#ifndef MESHTASTIC_TRACE_LOGGING
|
||||
#ifdef ARCH_PORTDUINO
|
||||
#define MESHTASTIC_TRACE_LOGGING 1
|
||||
#else
|
||||
#define MESHTASTIC_TRACE_LOGGING 0
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef USE_SEGGER
|
||||
// #undef DEBUG_PORT
|
||||
#define LOG_DEBUG(...) SEGGER_RTT_printf(0, __VA_ARGS__)
|
||||
@@ -55,16 +65,24 @@ extern MemGet memGet;
|
||||
#define LOG_WARN(...) SEGGER_RTT_printf(0, __VA_ARGS__)
|
||||
#define LOG_ERROR(...) SEGGER_RTT_printf(0, __VA_ARGS__)
|
||||
#define LOG_CRIT(...) SEGGER_RTT_printf(0, __VA_ARGS__)
|
||||
#if MESHTASTIC_TRACE_LOGGING
|
||||
#define LOG_TRACE(...) SEGGER_RTT_printf(0, __VA_ARGS__)
|
||||
#else
|
||||
#define LOG_TRACE(...)
|
||||
#endif
|
||||
#else
|
||||
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
|
||||
#define LOG_DEBUG(...) DEBUG_PORT.log(MESHTASTIC_LOG_LEVEL_DEBUG, __VA_ARGS__)
|
||||
#define LOG_INFO(...) DEBUG_PORT.log(MESHTASTIC_LOG_LEVEL_INFO, __VA_ARGS__)
|
||||
#define LOG_WARN(...) DEBUG_PORT.log(MESHTASTIC_LOG_LEVEL_WARN, __VA_ARGS__)
|
||||
#define LOG_ERROR(...) DEBUG_PORT.log(MESHTASTIC_LOG_LEVEL_ERROR, __VA_ARGS__)
|
||||
#define LOG_CRIT(...) DEBUG_PORT.log(MESHTASTIC_LOG_LEVEL_CRIT, __VA_ARGS__)
|
||||
#if MESHTASTIC_TRACE_LOGGING
|
||||
#define LOG_TRACE(...) DEBUG_PORT.log(MESHTASTIC_LOG_LEVEL_TRACE, __VA_ARGS__)
|
||||
#else
|
||||
#define LOG_TRACE(...)
|
||||
#endif
|
||||
#else
|
||||
#define LOG_DEBUG(...)
|
||||
#define LOG_INFO(...)
|
||||
#define LOG_WARN(...)
|
||||
|
||||
+2
-2
@@ -340,7 +340,7 @@ void listDir(const char *dirname, uint8_t levels, bool del)
|
||||
file.close();
|
||||
FSCom.remove(buffer);
|
||||
} else {
|
||||
LOG_DEBUG(" %s (%i Bytes)", filepath, file.size());
|
||||
LOG_TRACE(" %s (%i Bytes)", filepath, file.size());
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
@@ -394,7 +394,7 @@ void fsInit()
|
||||
#if defined(ARCH_ESP32)
|
||||
LOG_DEBUG("Filesystem files (%d/%d Bytes):", FSCom.usedBytes(), FSCom.totalBytes());
|
||||
#else
|
||||
LOG_DEBUG("Filesystem files:");
|
||||
LOG_TRACE("Filesystem files:");
|
||||
#endif
|
||||
listDir("/", 10);
|
||||
#endif
|
||||
|
||||
+9
-5
@@ -2,6 +2,7 @@
|
||||
#include "NodeDB.h"
|
||||
#include "Status.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/GPSLog.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
namespace meshtastic
|
||||
@@ -17,6 +18,7 @@ class GPSStatus : public Status
|
||||
|
||||
bool hasLock = false; // default to false, until we complete our first read
|
||||
bool isConnected = false; // Do we have a GPS we are talking to
|
||||
bool hasTime = false; // GPS has decoded a valid time this acquisition, even without a position fix
|
||||
|
||||
bool isPowerSaving = false; // Are we in power saving state
|
||||
|
||||
@@ -29,11 +31,12 @@ class GPSStatus : public Status
|
||||
GPSStatus() { statusType = STATUS_TYPE_GPS; }
|
||||
|
||||
// preferred method
|
||||
GPSStatus(bool hasLock, bool isConnected, bool isPowerSaving, const meshtastic_Position &pos) : Status()
|
||||
GPSStatus(bool hasLock, bool isConnected, bool isPowerSaving, const meshtastic_Position &pos, bool hasTime = false) : Status()
|
||||
{
|
||||
this->hasLock = hasLock;
|
||||
this->isConnected = isConnected;
|
||||
this->isPowerSaving = isPowerSaving;
|
||||
this->hasTime = hasTime;
|
||||
|
||||
// all-in-one struct copy
|
||||
this->p = pos;
|
||||
@@ -50,6 +53,8 @@ class GPSStatus : public Status
|
||||
|
||||
bool getIsPowerSaving() const { return isPowerSaving; }
|
||||
|
||||
bool getHasTime() const { return hasTime; }
|
||||
|
||||
int32_t getLatitude() const
|
||||
{
|
||||
if (config.position.fixed_position) {
|
||||
@@ -88,10 +93,8 @@ class GPSStatus : public Status
|
||||
|
||||
bool matches(const GPSStatus *newStatus) const
|
||||
{
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG("GPSStatus.match() new pos@%x to old pos@%x", newStatus->p.timestamp, p.timestamp);
|
||||
#endif
|
||||
return (newStatus->hasLock != hasLock || newStatus->isConnected != isConnected ||
|
||||
LOG_DEBUG_GPS("GPSStatus.match() new pos@%x to old pos@%x", newStatus->p.timestamp, p.timestamp);
|
||||
return (newStatus->hasLock != hasLock || newStatus->isConnected != isConnected || newStatus->hasTime != hasTime ||
|
||||
newStatus->isPowerSaving != isPowerSaving || newStatus->p.latitude_i != p.latitude_i ||
|
||||
newStatus->p.longitude_i != p.longitude_i || newStatus->p.altitude != p.altitude ||
|
||||
newStatus->p.altitude_hae != p.altitude_hae || newStatus->p.PDOP != p.PDOP ||
|
||||
@@ -112,6 +115,7 @@ class GPSStatus : public Status
|
||||
initialized = true;
|
||||
hasLock = newStatus->hasLock;
|
||||
isConnected = newStatus->isConnected;
|
||||
hasTime = newStatus->hasTime;
|
||||
|
||||
p = newStatus->p;
|
||||
|
||||
|
||||
+84
-30
@@ -171,7 +171,7 @@ static bool initAdcCalibration()
|
||||
}
|
||||
#endif
|
||||
|
||||
LOG_INFO("ADC calibration not supported; using approximate scaling");
|
||||
LOG_INFO("ADC calibration unsupported; use approx scaling");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -606,7 +606,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
// get current flow from INA sensor - negative value means power flowing
|
||||
// into the battery default assuming BATTERY+ <--> INA_VIN+ <--> SHUNT
|
||||
// RESISTOR <--> INA_VIN- <--> LOAD
|
||||
LOG_DEBUG("Using INA on I2C addr 0x%x for charging detection", config.power.device_battery_ina_address);
|
||||
LOG_TRACE("Using INA on I2C addr 0x%x for charging detection", config.power.device_battery_ina_address);
|
||||
#if defined(INA_CHARGING_DETECTION_INVERT)
|
||||
return getINACurrent() > 0;
|
||||
#else
|
||||
@@ -837,12 +837,13 @@ bool Power::setup()
|
||||
|
||||
void Power::powerCommandsCheck()
|
||||
{
|
||||
if (rebootAtMsec && millis() > rebootAtMsec) {
|
||||
// 0 means "not scheduled" for both, and reads as long expired - test it first.
|
||||
if (rebootAtMsec && Throttle::deadlinePassed(rebootAtMsec)) {
|
||||
LOG_INFO("Rebooting");
|
||||
reboot();
|
||||
}
|
||||
|
||||
if (shutdownAtMsec && millis() > shutdownAtMsec) {
|
||||
if (shutdownAtMsec && Throttle::deadlinePassed(shutdownAtMsec)) {
|
||||
shutdownAtMsec = 0;
|
||||
shutdown();
|
||||
}
|
||||
@@ -879,12 +880,13 @@ void Power::reboot()
|
||||
if (screen) {
|
||||
screen = nullptr;
|
||||
}
|
||||
LOG_DEBUG("final reboot!");
|
||||
LOG_DEBUG("final reboot");
|
||||
::reboot();
|
||||
#elif defined(ARCH_STM32)
|
||||
HAL_NVIC_SystemReset();
|
||||
#else
|
||||
rebootAtMsec = -1;
|
||||
// 0 disarms; UINT32_MAX would read as long expired and reboot-loop.
|
||||
rebootAtMsec = 0;
|
||||
LOG_WARN("FIXME implement reboot for this platform. Note that some settings "
|
||||
"require a restart to be applied");
|
||||
#endif
|
||||
@@ -1117,6 +1119,7 @@ int32_t Power::runOnce()
|
||||
{
|
||||
readPowerStatus();
|
||||
logHeapUsage();
|
||||
lipoChargerRetry();
|
||||
|
||||
#ifdef HAS_PMU
|
||||
// WE no longer use the IRQ line to wake the CPU (due to false wakes from
|
||||
@@ -1733,13 +1736,32 @@ bool Power::cw2015Init()
|
||||
|
||||
#if defined(HAS_PPM) && HAS_PPM
|
||||
|
||||
// The gauge is soldered on, so a failed init means wedged rather than absent - retry from
|
||||
// the power thread before writing it off.
|
||||
#define BQ27220_INIT_ATTEMPTS 3
|
||||
#define BQ27220_RETRY_INTERVAL_MS (60 * 1000)
|
||||
|
||||
/**
|
||||
* Adapter class for BQ25896/BQ27220 Lipo battery charger.
|
||||
*
|
||||
* The gauge only adds time-to-full/empty, so its failure must not take the charger down.
|
||||
*/
|
||||
class LipoCharger : public HasBatteryLevel
|
||||
{
|
||||
private:
|
||||
BQ27220 *bq = nullptr;
|
||||
uint8_t gaugeAttemptsLeft = BQ27220_INIT_ATTEMPTS;
|
||||
uint32_t lastGaugeAttemptMs = 0;
|
||||
|
||||
// An aborted transfer leaves the i2c_master driver holding a stale transaction, which
|
||||
// the next transfer trips over. Deleting the bus frees it along with the interrupt.
|
||||
void recoverI2CBus()
|
||||
{
|
||||
#ifdef ARCH_ESP32
|
||||
Wire.end();
|
||||
Wire.begin(I2C_SDA, I2C_SCL);
|
||||
#endif
|
||||
}
|
||||
|
||||
public:
|
||||
/**
|
||||
@@ -1786,24 +1808,46 @@ class LipoCharger : public HasBatteryLevel
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (bq == nullptr) {
|
||||
bq = new BQ27220;
|
||||
bq->setDefaultCapacity(BQ27220_DESIGN_CAPACITY);
|
||||
gaugeRunOnce();
|
||||
// Ready on the charger alone, so Power stays enabled and can retry the gauge later.
|
||||
return true;
|
||||
}
|
||||
|
||||
bool result = bq->init();
|
||||
if (result) {
|
||||
LOG_DEBUG("BQ27220 design capacity: %d", bq->getDesignCapacity());
|
||||
LOG_DEBUG("BQ27220 fullCharge capacity: %d", bq->getFullChargeCapacity());
|
||||
LOG_DEBUG("BQ27220 remaining capacity: %d", bq->getRemainingCapacity());
|
||||
return true;
|
||||
} else {
|
||||
LOG_WARN("BQ27220 init failed");
|
||||
delete bq;
|
||||
bq = nullptr;
|
||||
return false;
|
||||
}
|
||||
/// Bring up the BQ27220 fuel gauge, unless it is already up or out of attempts
|
||||
void gaugeRunOnce()
|
||||
{
|
||||
if (bq != nullptr || gaugeAttemptsLeft == 0)
|
||||
return;
|
||||
if (gaugeAttemptsLeft < BQ27220_INIT_ATTEMPTS &&
|
||||
Throttle::isWithinTimespanMs(lastGaugeAttemptMs, BQ27220_RETRY_INTERVAL_MS))
|
||||
return;
|
||||
|
||||
lastGaugeAttemptMs = millis();
|
||||
gaugeAttemptsLeft--;
|
||||
|
||||
// Cheap probe first: a silent gauge costs one transaction instead of the
|
||||
// multi-second unseal/reset/provision sequence inside init().
|
||||
Wire.beginTransmission(BQ27220_I2C_ADDRESS);
|
||||
if (Wire.endTransmission() != 0) {
|
||||
LOG_WARN("BQ27220 not responding at 0x%x", BQ27220_I2C_ADDRESS);
|
||||
return;
|
||||
}
|
||||
return false;
|
||||
|
||||
bq = new BQ27220;
|
||||
bq->setDefaultCapacity(BQ27220_DESIGN_CAPACITY);
|
||||
|
||||
if (bq->init()) {
|
||||
LOG_DEBUG("BQ27220 design capacity: %d", bq->getDesignCapacity());
|
||||
LOG_DEBUG("BQ27220 fullCharge capacity: %d", bq->getFullChargeCapacity());
|
||||
LOG_DEBUG("BQ27220 remaining capacity: %d", bq->getRemainingCapacity());
|
||||
return;
|
||||
}
|
||||
|
||||
delete bq;
|
||||
bq = nullptr;
|
||||
// init() bails out mid-sequence, so hand the next bus user a sane driver state.
|
||||
recoverI2CBus();
|
||||
LOG_WARN("BQ27220 init failed (%d retries left), use BQ25896 for battery state", (int)gaugeAttemptsLeft);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1819,7 +1863,7 @@ class LipoCharger : public HasBatteryLevel
|
||||
/**
|
||||
* The raw voltage of the battery in millivolts, or NAN if unknown
|
||||
*/
|
||||
virtual uint16_t getBattVoltage() override { return bq->getVoltage(); }
|
||||
virtual uint16_t getBattVoltage() override { return bq ? bq->getVoltage() : PPM->getBattVoltage(); }
|
||||
|
||||
/**
|
||||
* return true if there is a battery installed in this unit
|
||||
@@ -1837,11 +1881,13 @@ class LipoCharger : public HasBatteryLevel
|
||||
virtual bool isCharging() override
|
||||
{
|
||||
bool isCharging = PPM->isCharging();
|
||||
if (isCharging) {
|
||||
LOG_DEBUG("BQ27220 time to full charge: %d min", bq->getTimeToFull());
|
||||
} else {
|
||||
if (!PPM->isVbusIn()) {
|
||||
LOG_DEBUG("BQ27220 time to empty: %d min (%d mAh)", bq->getTimeToEmpty(), bq->getRemainingCapacity());
|
||||
if (bq) {
|
||||
if (isCharging) {
|
||||
LOG_TRACE("BQ27220 time to full charge: %d min", bq->getTimeToFull());
|
||||
} else {
|
||||
if (!PPM->isVbusIn()) {
|
||||
LOG_TRACE("BQ27220 time to empty: %d min (%d mAh)", bq->getTimeToEmpty(), bq->getRemainingCapacity());
|
||||
}
|
||||
}
|
||||
}
|
||||
return isCharging;
|
||||
@@ -1863,6 +1909,12 @@ bool Power::lipoChargerInit()
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Retry a fuel gauge that did not come up during setup
|
||||
void Power::lipoChargerRetry()
|
||||
{
|
||||
lipoCharger.gaugeRunOnce();
|
||||
}
|
||||
|
||||
#else
|
||||
/**
|
||||
* The Lipo battery level sensor is unavailable - default to AnalogBatteryLevel
|
||||
@@ -1871,6 +1923,8 @@ bool Power::lipoChargerInit()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void Power::lipoChargerRetry() {}
|
||||
#endif
|
||||
|
||||
#ifdef HELTEC_MESH_SOLAR
|
||||
@@ -1926,7 +1980,7 @@ meshSolarBatteryLevel meshSolarLevel;
|
||||
bool Power::meshSolarInit()
|
||||
{
|
||||
bool result = meshSolarLevel.runOnce();
|
||||
LOG_DEBUG("Power::meshSolarInit mesh solar sensor is %s", result ? "ready" : "not ready yet");
|
||||
LOG_DEBUG("Power::meshSolarInit sensor is %s", result ? "ready" : "not ready yet");
|
||||
if (!result)
|
||||
return false;
|
||||
batteryLevel = &meshSolarLevel;
|
||||
@@ -2059,7 +2113,7 @@ bool Power::serialBatteryInit()
|
||||
#endif
|
||||
|
||||
bool result = serialBatteryLevel.runOnce();
|
||||
LOG_DEBUG("Power::serialBatteryInit serial battery sensor is %s", result ? "ready" : "not ready yet");
|
||||
LOG_DEBUG("Power::serialBatteryInit sensor is %s", result ? "ready" : "not ready yet");
|
||||
if (!result)
|
||||
return false;
|
||||
batteryLevel = &serialBatteryLevel;
|
||||
|
||||
@@ -121,6 +121,8 @@ class Power : public concurrency::OSThread
|
||||
bool max17048Init();
|
||||
/// Setup a Lipo charger
|
||||
bool lipoChargerInit();
|
||||
/// Retry a fuel gauge that did not come up during setup
|
||||
void lipoChargerRetry();
|
||||
/// Setup a meshSolar battery sensor
|
||||
bool meshSolarInit();
|
||||
/// Setup a serial battery sensor
|
||||
|
||||
+1
-1
@@ -165,7 +165,7 @@ static void lsIdle()
|
||||
wakeCause2 = doLightSleep(100); // leave led on for 1ms
|
||||
|
||||
secsSlept += sleepTime;
|
||||
// LOG_INFO("Sleep, flash led!");
|
||||
// LOG_INFO("Sleep, flash led");
|
||||
break;
|
||||
|
||||
case ESP_SLEEP_WAKEUP_UART:
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "configuration.h"
|
||||
#include "main.h"
|
||||
#include "mesh/Throttle.h"
|
||||
|
||||
namespace concurrency
|
||||
{
|
||||
@@ -29,9 +30,9 @@ class PowerFSMThread : public OSThread
|
||||
if (powerStatus->getHasUSB()) {
|
||||
timeLastPowered = millis();
|
||||
} else if (config.power.on_battery_shutdown_after_secs > 0 && config.power.on_battery_shutdown_after_secs != UINT32_MAX &&
|
||||
millis() > (timeLastPowered +
|
||||
Default::getConfiguredOrDefaultMs(
|
||||
config.power.on_battery_shutdown_after_secs))) { // shutdown after 30 minutes unpowered
|
||||
Throttle::hasElapsed(
|
||||
timeLastPowered,
|
||||
Default::getConfiguredOrDefaultMs(config.power.on_battery_shutdown_after_secs))) { // unpowered too long
|
||||
powerFSM.trigger(EVENT_SHUTDOWN);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ bool SafeFile::close()
|
||||
String filenameTmp = filename;
|
||||
filenameTmp += ".tmp";
|
||||
if (!renameFile(filenameTmp.c_str(), filename.c_str())) {
|
||||
LOG_ERROR("Error: can't rename new pref file");
|
||||
LOG_ERROR("Can't rename new pref file");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
+82
-17
@@ -1,33 +1,98 @@
|
||||
// See UptimeClock.h for the full contract.
|
||||
#include "UptimeClock.h"
|
||||
#include <Arduino.h>
|
||||
#include <atomic>
|
||||
|
||||
uint32_t Time::getMillis()
|
||||
{
|
||||
#ifdef PIO_UNIT_TESTING
|
||||
if (Time::useTestClock)
|
||||
return Time::testNowMs;
|
||||
if (Time::useTestClock.load(std::memory_order_relaxed))
|
||||
return Time::testNowMs.load(std::memory_order_relaxed);
|
||||
#endif
|
||||
return millis();
|
||||
}
|
||||
|
||||
uint64_t Time::getMillis64()
|
||||
namespace
|
||||
{
|
||||
static uint32_t lastLow = 0; // last 32-bit sample
|
||||
static uint32_t highWord = 0; // number of observed wraps
|
||||
struct PublishedSnapshot {
|
||||
std::atomic<uint32_t> high{0};
|
||||
std::atomic<uint32_t> low{0};
|
||||
};
|
||||
|
||||
uint32_t now = Time::getMillis();
|
||||
// The constexpr atomic initializers make both snapshots available before firmware startup.
|
||||
PublishedSnapshot published[2];
|
||||
std::atomic<uint32_t> publishedGeneration{0};
|
||||
#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;
|
||||
}
|
||||
std::atomic<Time::MonotonicPublishHook> monotonicPublishHook{nullptr};
|
||||
#endif
|
||||
if (now < lastLow)
|
||||
highWord++; // low word wrapped since last call
|
||||
lastLow = now;
|
||||
return (static_cast<uint64_t>(highWord) << 32) | now;
|
||||
|
||||
// Extend a published (high, low) snapshot to `now`; unsigned subtraction is exact across the wrap
|
||||
// for any gap under 49.7 days. One copy, because reader and writer must agree on it exactly.
|
||||
uint64_t extendPublished(uint32_t high, uint32_t low, uint32_t now)
|
||||
{
|
||||
return ((((uint64_t)high << 32) | low) + (uint32_t)(now - low));
|
||||
}
|
||||
|
||||
// A generation change means the writer completed a publish while this copy was being read. A
|
||||
// paused publish leaves the generation unchanged and writes only the inactive snapshot.
|
||||
void readPublished(uint32_t &high, uint32_t &low)
|
||||
{
|
||||
for (;;) {
|
||||
const uint32_t before = publishedGeneration.load(std::memory_order_acquire);
|
||||
PublishedSnapshot &snapshot = published[before & 1u];
|
||||
high = snapshot.high.load(std::memory_order_relaxed);
|
||||
low = snapshot.low.load(std::memory_order_relaxed);
|
||||
std::atomic_thread_fence(std::memory_order_acquire);
|
||||
if (publishedGeneration.load(std::memory_order_relaxed) == before)
|
||||
return;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
uint64_t Time::getMillisMonotonic()
|
||||
{
|
||||
uint32_t high, low;
|
||||
readPublished(high, low);
|
||||
// The reader writes nothing back; it just extends the last published carry to now.
|
||||
return extendPublished(high, low, getMillis());
|
||||
}
|
||||
|
||||
uint32_t Time::getUptimeSecs()
|
||||
{
|
||||
return (uint32_t)(getMillisMonotonic() / 1000);
|
||||
}
|
||||
|
||||
void Time::serviceMonotonic()
|
||||
{
|
||||
const uint32_t generation = publishedGeneration.load(std::memory_order_relaxed);
|
||||
PublishedSnapshot &active = published[generation & 1u];
|
||||
const uint32_t low = active.low.load(std::memory_order_relaxed);
|
||||
const uint32_t high = active.high.load(std::memory_order_relaxed);
|
||||
const uint64_t next = extendPublished(high, low, getMillis());
|
||||
|
||||
PublishedSnapshot &inactive = published[(generation + 1u) & 1u];
|
||||
inactive.high.store((uint32_t)(next >> 32), std::memory_order_relaxed);
|
||||
inactive.low.store((uint32_t)next, std::memory_order_relaxed);
|
||||
#ifdef PIO_UNIT_TESTING
|
||||
if (const auto hook = monotonicPublishHook.load(std::memory_order_relaxed))
|
||||
hook();
|
||||
#endif
|
||||
publishedGeneration.store(generation + 1u, std::memory_order_release);
|
||||
}
|
||||
|
||||
#ifdef PIO_UNIT_TESTING
|
||||
void Time::resetMonotonicForTests()
|
||||
{
|
||||
publishedGeneration.store(0, std::memory_order_relaxed);
|
||||
for (auto &snapshot : published) {
|
||||
snapshot.high.store(0, std::memory_order_relaxed);
|
||||
snapshot.low.store(0, std::memory_order_relaxed);
|
||||
}
|
||||
monotonicPublishHook.store(nullptr, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void Time::setMonotonicPublishHookForTests(MonotonicPublishHook hook)
|
||||
{
|
||||
monotonicPublishHook.store(hook, std::memory_order_relaxed);
|
||||
}
|
||||
#endif
|
||||
+43
-20
@@ -1,46 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#ifdef PIO_UNIT_TESTING
|
||||
#include <atomic>
|
||||
#endif
|
||||
|
||||
// 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
|
||||
// Test-only virtual clock; OFF by default so suites relying on real time are unaffected. Atomic so
|
||||
// a suite can step the clock from one thread while others read it - the concurrent-reader cases in
|
||||
// test_uptime_clock/ do exactly that.
|
||||
inline std::atomic<uint32_t> testNowMs{0};
|
||||
inline std::atomic<bool> useTestClock{false};
|
||||
using MonotonicPublishHook = void (*)();
|
||||
|
||||
inline void setTestMillis(uint32_t ms)
|
||||
{
|
||||
testNowMs = ms;
|
||||
useTestClock = true;
|
||||
clockSourceChanged = true;
|
||||
testNowMs.store(ms, std::memory_order_relaxed);
|
||||
useTestClock.store(true, std::memory_order_relaxed);
|
||||
}
|
||||
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;
|
||||
testNowMs.fetch_add(deltaMs, std::memory_order_relaxed);
|
||||
useTestClock.store(true, std::memory_order_relaxed);
|
||||
}
|
||||
// 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;
|
||||
useTestClock.store(false, std::memory_order_relaxed);
|
||||
testNowMs.store(0, std::memory_order_relaxed);
|
||||
}
|
||||
// Zero the published wrap carry. Suites that assert absolute uptime values call this in setUp():
|
||||
// a previous case that moved the test clock backwards left a counted wrap behind.
|
||||
void resetMonotonicForTests();
|
||||
void setMonotonicPublishHookForTests(MonotonicPublishHook hook);
|
||||
#endif
|
||||
|
||||
/// Milliseconds since boot, 32-bit (wraps ~49.7 days). Drop-in for millis().
|
||||
/// Milliseconds since boot, 32-bit (wraps ~49.7 days). Drop-in for millis(). For "has this interval
|
||||
/// elapsed / deadline arrived" use Throttle (isWithinTimespanMs / hasElapsed / deadlinePassed),
|
||||
/// which is wrap-correct with no carry state at all.
|
||||
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();
|
||||
/// Milliseconds since boot as a monotonic 64-bit count.
|
||||
///
|
||||
/// A pure read: it derives its answer from a complete snapshot published by serviceMonotonic()
|
||||
/// plus the unsigned elapsed time since that snapshot, which is exact across the wrap. A reader
|
||||
/// that preempts publication uses the previous snapshot. If publication completes during a copy,
|
||||
/// the reader retries; it never waits for a publish in progress.
|
||||
///
|
||||
/// Not intended for ISR call sites because lock-free std::atomic operations are not guaranteed by
|
||||
/// every supported toolchain. ISRs use getMillis(); the publication protocol itself never waits.
|
||||
uint64_t getMillisMonotonic();
|
||||
|
||||
/// Whole seconds since boot, derived from getMillisMonotonic() (~136 years of range). This is
|
||||
/// the unit to store when an instant must be dated before the wall clock is trustworthy.
|
||||
uint32_t getUptimeSecs();
|
||||
|
||||
/// Advances the published wrap carry. THE ONLY WRITER - call it from the main loop and nowhere
|
||||
/// else. Two concurrent callers could count one wrap twice, jumping every uptime and wall-clock
|
||||
/// reading ~49.7 days forward for the rest of the boot.
|
||||
///
|
||||
/// Must run at least once per ~49.7-day wrap window; the main loop calls it every iteration.
|
||||
void serviceMonotonic();
|
||||
|
||||
} // namespace Time
|
||||
+99
-65
@@ -1,6 +1,8 @@
|
||||
#include "airtime.h"
|
||||
#include "NodeDB.h"
|
||||
#include "UptimeClock.h"
|
||||
#include "configuration.h"
|
||||
#include <string.h>
|
||||
|
||||
AirTime *airTime = NULL;
|
||||
|
||||
@@ -11,6 +13,9 @@ uint32_t air_period_rx[PERIODS_TO_LOG];
|
||||
|
||||
void AirTime::logAirtime(reportTypes reportType, uint32_t airtime_ms)
|
||||
{
|
||||
// A packet may be logged immediately after waking from light sleep. Sync first so
|
||||
// the packet is counted in the current wall-time bucket, not a stale awake-time bucket.
|
||||
syncNow();
|
||||
|
||||
if (reportType == TX_LOG) {
|
||||
LOG_DEBUG("Packet TX: %ums", airtime_ms);
|
||||
@@ -33,47 +38,112 @@ void AirTime::logAirtime(reportTypes reportType, uint32_t airtime_ms)
|
||||
|
||||
uint8_t AirTime::currentPeriodIndex()
|
||||
{
|
||||
return ((getSecondsSinceBoot() / SECONDS_PER_PERIOD) % PERIODS_TO_LOG);
|
||||
return ((secSinceBoot / SECONDS_PER_PERIOD) % PERIODS_TO_LOG);
|
||||
}
|
||||
|
||||
uint8_t AirTime::getPeriodUtilMinute()
|
||||
{
|
||||
return (getSecondsSinceBoot() / 10) % CHANNEL_UTILIZATION_PERIODS;
|
||||
return (secSinceBoot / 10) % CHANNEL_UTILIZATION_PERIODS;
|
||||
}
|
||||
|
||||
uint8_t AirTime::getPeriodUtilHour()
|
||||
{
|
||||
return (getSecondsSinceBoot() / 60) % MINUTES_IN_HOUR;
|
||||
return (secSinceBoot / 60) % MINUTES_IN_HOUR;
|
||||
}
|
||||
|
||||
void AirTime::airtimeRotatePeriod()
|
||||
{
|
||||
// Preserve the public helper while keeping all rotation logic in one monotonic-time path.
|
||||
syncNow();
|
||||
}
|
||||
|
||||
if (this->airtimes.lastPeriodIndex != this->currentPeriodIndex()) {
|
||||
LOG_DEBUG("Rotate airtimes to a new period = %u", this->currentPeriodIndex());
|
||||
void AirTime::syncNow()
|
||||
{
|
||||
// Monotonic uptime, not RTC/network time: a user, GPS, or NTP clock change must not move
|
||||
// airtime accounting. Pure read; the main loop publishes the wrap carry it derives from.
|
||||
uint32_t nowSecs = Time::getUptimeSecs();
|
||||
|
||||
for (int i = PERIODS_TO_LOG - 2; i >= 0; --i) {
|
||||
this->airtimes.periodTX[i + 1] = this->airtimes.periodTX[i];
|
||||
this->airtimes.periodRX[i + 1] = this->airtimes.periodRX[i];
|
||||
this->airtimes.periodRX_ALL[i + 1] = this->airtimes.periodRX_ALL[i];
|
||||
|
||||
air_period_tx[i + 1] = this->airtimes.periodTX[i];
|
||||
air_period_rx[i + 1] = this->airtimes.periodRX[i];
|
||||
}
|
||||
|
||||
this->airtimes.periodTX[0] = 0;
|
||||
this->airtimes.periodRX[0] = 0;
|
||||
this->airtimes.periodRX_ALL[0] = 0;
|
||||
|
||||
air_period_tx[0] = 0;
|
||||
air_period_rx[0] = 0;
|
||||
if (firstTime) {
|
||||
memset(this->utilizationTX, 0, sizeof(this->utilizationTX));
|
||||
memset(this->channelUtilization, 0, sizeof(this->channelUtilization));
|
||||
memset(this->airtimes.periodTX, 0, sizeof(this->airtimes.periodTX));
|
||||
memset(this->airtimes.periodRX, 0, sizeof(this->airtimes.periodRX));
|
||||
memset(this->airtimes.periodRX_ALL, 0, sizeof(this->airtimes.periodRX_ALL));
|
||||
memset(air_period_tx, 0, sizeof(air_period_tx));
|
||||
memset(air_period_rx, 0, sizeof(air_period_rx));
|
||||
|
||||
this->secSinceBoot = nowSecs;
|
||||
this->lastUtilPeriod = this->getPeriodUtilMinute();
|
||||
this->lastUtilPeriodTX = this->getPeriodUtilHour();
|
||||
this->airtimes.lastPeriodIndex = this->currentPeriodIndex();
|
||||
firstTime = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (nowSecs == this->secSinceBoot) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t oldSecSinceBoot = this->secSinceBoot;
|
||||
this->secSinceBoot = nowSecs;
|
||||
|
||||
// Historical airtime reports use 1-hour buckets. If multiple hours elapsed while
|
||||
// asleep, rotate each crossed bucket or clear the whole report window.
|
||||
uint32_t elapsedAirtimePeriods = (this->secSinceBoot / SECONDS_PER_PERIOD) - (oldSecSinceBoot / SECONDS_PER_PERIOD);
|
||||
if (elapsedAirtimePeriods >= PERIODS_TO_LOG) {
|
||||
memset(this->airtimes.periodTX, 0, sizeof(this->airtimes.periodTX));
|
||||
memset(this->airtimes.periodRX, 0, sizeof(this->airtimes.periodRX));
|
||||
memset(this->airtimes.periodRX_ALL, 0, sizeof(this->airtimes.periodRX_ALL));
|
||||
memset(air_period_tx, 0, sizeof(air_period_tx));
|
||||
memset(air_period_rx, 0, sizeof(air_period_rx));
|
||||
} else {
|
||||
while (elapsedAirtimePeriods-- > 0) {
|
||||
LOG_DEBUG("Rotate airtimes to a new period = %u", this->currentPeriodIndex());
|
||||
for (int i = PERIODS_TO_LOG - 2; i >= 0; --i) {
|
||||
this->airtimes.periodTX[i + 1] = this->airtimes.periodTX[i];
|
||||
this->airtimes.periodRX[i + 1] = this->airtimes.periodRX[i];
|
||||
this->airtimes.periodRX_ALL[i + 1] = this->airtimes.periodRX_ALL[i];
|
||||
air_period_tx[i + 1] = this->airtimes.periodTX[i];
|
||||
air_period_rx[i + 1] = this->airtimes.periodRX[i];
|
||||
}
|
||||
|
||||
this->airtimes.periodTX[0] = 0;
|
||||
this->airtimes.periodRX[0] = 0;
|
||||
this->airtimes.periodRX_ALL[0] = 0;
|
||||
air_period_tx[0] = 0;
|
||||
air_period_rx[0] = 0;
|
||||
}
|
||||
}
|
||||
this->airtimes.lastPeriodIndex = this->currentPeriodIndex();
|
||||
|
||||
// Channel utilization is a rolling 60-second view split into six 10-second buckets.
|
||||
// Clear every bucket crossed while asleep so old airtime decays by real elapsed time.
|
||||
uint32_t elapsedUtilPeriods = (this->secSinceBoot / 10) - (oldSecSinceBoot / 10);
|
||||
if (elapsedUtilPeriods >= CHANNEL_UTILIZATION_PERIODS) {
|
||||
memset(this->channelUtilization, 0, sizeof(this->channelUtilization));
|
||||
} else {
|
||||
for (uint32_t i = 1; i <= elapsedUtilPeriods; i++) {
|
||||
this->channelUtilization[((oldSecSinceBoot / 10) + i) % CHANNEL_UTILIZATION_PERIODS] = 0;
|
||||
}
|
||||
}
|
||||
this->lastUtilPeriod = this->getPeriodUtilMinute();
|
||||
|
||||
// TX utilization is a rolling 60-minute view used by duty-cycle checks.
|
||||
uint32_t elapsedUtilTXPeriods = (this->secSinceBoot / 60) - (oldSecSinceBoot / 60);
|
||||
if (elapsedUtilTXPeriods >= MINUTES_IN_HOUR) {
|
||||
memset(this->utilizationTX, 0, sizeof(this->utilizationTX));
|
||||
} else {
|
||||
for (uint32_t i = 1; i <= elapsedUtilTXPeriods; i++) {
|
||||
this->utilizationTX[((oldSecSinceBoot / 60) + i) % MINUTES_IN_HOUR] = 0;
|
||||
}
|
||||
}
|
||||
this->lastUtilPeriodTX = this->getPeriodUtilHour();
|
||||
}
|
||||
|
||||
uint32_t *AirTime::airtimeReport(reportTypes reportType)
|
||||
{
|
||||
// Reports may be requested before runOnce() executes after wake.
|
||||
syncNow();
|
||||
|
||||
if (reportType == TX_LOG) {
|
||||
return this->airtimes.periodTX;
|
||||
@@ -97,11 +167,16 @@ uint32_t AirTime::getSecondsPerPeriod()
|
||||
|
||||
uint32_t AirTime::getSecondsSinceBoot()
|
||||
{
|
||||
// Keep HTTP/debug reporting aligned with the same monotonic clock used by the buckets.
|
||||
syncNow();
|
||||
return this->secSinceBoot;
|
||||
}
|
||||
|
||||
float AirTime::channelUtilizationPercent()
|
||||
{
|
||||
// Gate decisions should see buckets that have decayed across light-sleep time.
|
||||
syncNow();
|
||||
|
||||
uint32_t sum = 0;
|
||||
for (uint32_t i = 0; i < CHANNEL_UTILIZATION_PERIODS; i++) {
|
||||
sum += this->channelUtilization[i];
|
||||
@@ -112,6 +187,9 @@ float AirTime::channelUtilizationPercent()
|
||||
|
||||
float AirTime::utilizationTXPercent()
|
||||
{
|
||||
// Duty-cycle checks use this value, so keep it current even outside the periodic thread.
|
||||
syncNow();
|
||||
|
||||
uint32_t sum = 0;
|
||||
for (uint32_t i = 0; i < MINUTES_IN_HOUR; i++) {
|
||||
sum += this->utilizationTX[i];
|
||||
@@ -162,50 +240,6 @@ AirTime::AirTime() : concurrency::OSThread("AirTime"), airtimes({}) {}
|
||||
|
||||
int32_t AirTime::runOnce()
|
||||
{
|
||||
secSinceBoot++;
|
||||
|
||||
uint8_t utilPeriod = this->getPeriodUtilMinute();
|
||||
uint8_t utilPeriodTX = this->getPeriodUtilHour();
|
||||
|
||||
if (firstTime) {
|
||||
|
||||
// Init utilizationTX window to all 0
|
||||
for (uint32_t i = 0; i < MINUTES_IN_HOUR; i++) {
|
||||
this->utilizationTX[i] = 0;
|
||||
}
|
||||
|
||||
// Init channelUtilization window to all 0
|
||||
for (uint32_t i = 0; i < CHANNEL_UTILIZATION_PERIODS; i++) {
|
||||
this->channelUtilization[i] = 0;
|
||||
}
|
||||
|
||||
// Init airtime windows to all 0
|
||||
for (int i = 0; i < PERIODS_TO_LOG; i++) {
|
||||
this->airtimes.periodTX[i] = 0;
|
||||
this->airtimes.periodRX[i] = 0;
|
||||
this->airtimes.periodRX_ALL[i] = 0;
|
||||
|
||||
// air_period_tx[i] = 0;
|
||||
// air_period_rx[i] = 0;
|
||||
}
|
||||
|
||||
firstTime = false;
|
||||
lastUtilPeriod = utilPeriod;
|
||||
} else {
|
||||
this->airtimeRotatePeriod();
|
||||
|
||||
// Reset the channelUtilization window when we roll over
|
||||
if (lastUtilPeriod != utilPeriod) {
|
||||
lastUtilPeriod = utilPeriod;
|
||||
|
||||
this->channelUtilization[utilPeriod] = 0;
|
||||
}
|
||||
|
||||
if (lastUtilPeriodTX != utilPeriodTX) {
|
||||
lastUtilPeriodTX = utilPeriodTX;
|
||||
|
||||
this->utilizationTX[utilPeriodTX] = 0;
|
||||
}
|
||||
}
|
||||
syncNow();
|
||||
return (1000 * 1);
|
||||
}
|
||||
@@ -39,6 +39,12 @@ void logAirtime(reportTypes reportType, uint32_t airtime_ms);
|
||||
|
||||
uint32_t *airtimeReport(reportTypes reportType);
|
||||
|
||||
// Not thread-safe: everything but getPeriodsToLog()/getSecondsPerPeriod() either rotates the
|
||||
// windows via syncNow() or reads the buckets. Current callers are all on the OSThread scheduler -
|
||||
// RadioLibInterface/SimRadio, RadioInterface, Router, DeviceTelemetry, ContentHandler, and the
|
||||
// screen renderers. New callers must be on that thread too, or this needs a lock.
|
||||
// TODO: airtime lock-guarding - serialise the above behind a lock so the contract is enforced
|
||||
// rather than documented. Kept out of this PR: it is a separate concern from millis() rollover.
|
||||
class AirTime : private concurrency::OSThread
|
||||
{
|
||||
|
||||
@@ -66,6 +72,8 @@ class AirTime : private concurrency::OSThread
|
||||
bool firstTime = true;
|
||||
uint8_t lastUtilPeriod = 0;
|
||||
uint8_t lastUtilPeriodTX = 0;
|
||||
// Time::getUptimeSecs() as of the last syncNow(); the gap since is what the windows rotate by,
|
||||
// so they stay correct even if the scheduler was paused by light sleep.
|
||||
uint32_t secSinceBoot = 0;
|
||||
uint8_t max_channel_util_percent = 40;
|
||||
uint8_t polite_channel_util_percent = 25;
|
||||
@@ -81,6 +89,8 @@ class AirTime : private concurrency::OSThread
|
||||
uint8_t getPeriodUtilMinute();
|
||||
uint8_t getPeriodUtilHour();
|
||||
uint8_t currentPeriodIndex();
|
||||
// Advance rolling airtime windows from monotonic uptime, not from runOnce() calls.
|
||||
void syncNow();
|
||||
|
||||
protected:
|
||||
virtual int32_t runOnce() override;
|
||||
|
||||
@@ -88,6 +88,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define MESHTASTIC_PREHOP_DROP 1
|
||||
#endif
|
||||
|
||||
// Use polynomial approximations for trigonometric functions to save flash.
|
||||
// Override with -D MESHTASTIC_TRIG_APPROX=0 for exact trig for special use cases e.g. close to Earth's poles.
|
||||
#ifndef MESHTASTIC_TRIG_APPROX
|
||||
#define MESHTASTIC_TRIG_APPROX 1
|
||||
#endif
|
||||
|
||||
// Debug/test only: let a wired client (serial/TCP) inject frames into the RX pipeline as if they had
|
||||
// arrived over LoRa - a SIMULATOR_APP ToRadio packet is delivered through the real receive path on real
|
||||
// hardware (see MeshService::injectAsReceived). This forges over-the-air traffic, so it MUST stay 0 in
|
||||
|
||||
@@ -35,20 +35,20 @@ class ReClockI2C
|
||||
uint32_t currentClock = this->getClock();
|
||||
|
||||
if (currentClock) {
|
||||
LOG_DEBUG("Current I2C frequency: %uHz", currentClock);
|
||||
LOG_TRACE("Current I2C frequency: %uHz", currentClock);
|
||||
}
|
||||
|
||||
if (currentClock != desiredClock) {
|
||||
LOG_DEBUG("Changing I2C clock to %uHz", desiredClock);
|
||||
LOG_TRACE("Changing I2C clock to %uHz", desiredClock);
|
||||
this->i2cBus->setClock(desiredClock);
|
||||
// If the clock is 0Hz, we still store it
|
||||
// We'll check in restoreClock function
|
||||
setPreviousClock(currentClock);
|
||||
LOG_DEBUG("Stored previous clock I2C clock: %uHz", this->previousClock);
|
||||
LOG_TRACE("Stored previous clock I2C clock: %uHz", this->previousClock);
|
||||
return true;
|
||||
}
|
||||
|
||||
LOG_DEBUG("I2C clock was already %uHz. Skipping", desiredClock);
|
||||
LOG_TRACE("I2C clock was already %uHz. Skipping", desiredClock);
|
||||
setPreviousClock(0);
|
||||
return false;
|
||||
}
|
||||
@@ -56,12 +56,12 @@ class ReClockI2C
|
||||
bool restoreClock()
|
||||
{
|
||||
if (this->previousClock) {
|
||||
LOG_DEBUG("Restoring I2C clock to %uHz", this->previousClock);
|
||||
LOG_TRACE("Restoring I2C clock to %uHz", this->previousClock);
|
||||
i2cBus->setClock(this->previousClock);
|
||||
setPreviousClock(0);
|
||||
return true;
|
||||
}
|
||||
LOG_DEBUG("I2C clock was unknown. Not restored");
|
||||
LOG_TRACE("I2C clock was unknown. Not restored");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -743,7 +743,6 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
logFoundDevice("DS2482-800", (uint8_t)addr.address);
|
||||
break;
|
||||
}
|
||||
|
||||
type = HMC5883L;
|
||||
logFoundDevice("HMC5883L", (uint8_t)addr.address);
|
||||
break;
|
||||
|
||||
+100
-114
@@ -5,10 +5,12 @@
|
||||
#if !MESHTASTIC_EXCLUDE_GPS
|
||||
#include "Default.h"
|
||||
#include "GPS.h"
|
||||
#include "GPSLog.h"
|
||||
#include "GpioLogic.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerMon.h"
|
||||
#include "Throttle.h"
|
||||
#include "UptimeClock.h"
|
||||
#include "buzz.h"
|
||||
#include "concurrency/Periodic.h"
|
||||
#include "gps/RTC.h"
|
||||
@@ -337,7 +339,7 @@ uint8_t GPS::makeCASPacket(uint8_t class_id, uint8_t msg_id, uint8_t payload_siz
|
||||
}
|
||||
CASChecksum(UBXscratch, (payload_size + 10));
|
||||
|
||||
#if defined(GPS_DEBUG) && defined(DEBUG_PORT)
|
||||
#if GPS_DEBUG && defined(DEBUG_PORT)
|
||||
LOG_DEBUG("CAS packet: ");
|
||||
DEBUG_PORT.hexDump(MESHTASTIC_LOG_LEVEL_DEBUG, UBXscratch, payload_size + 10);
|
||||
#endif
|
||||
@@ -349,27 +351,25 @@ GPS_RESPONSE GPS::getACK(const char *message, uint32_t waitMillis)
|
||||
uint8_t buffer[768] = {0};
|
||||
uint8_t b;
|
||||
int bytesRead = 0;
|
||||
uint32_t startTimeout = millis() + waitMillis;
|
||||
#ifdef GPS_DEBUG
|
||||
// Start stamp + interval rather than a stored deadline: same wrap-safety, but the full 49.7-day
|
||||
// range instead of 24.8 days ahead, and Time::getMillis() makes the wait injectable.
|
||||
const uint32_t waitStartMs = Time::getMillis();
|
||||
#if GPS_DEBUG
|
||||
std::string debugmsg = "";
|
||||
#endif
|
||||
while (millis() < startTimeout) {
|
||||
while (Throttle::isWithinTimespanMs(waitStartMs, waitMillis)) {
|
||||
if (_serial_gps->available()) {
|
||||
b = _serial_gps->read();
|
||||
|
||||
#ifdef GPS_DEBUG
|
||||
#if GPS_DEBUG
|
||||
debugmsg += vformat("%c", (b >= 32 && b <= 126) ? b : '.');
|
||||
#endif
|
||||
buffer[bytesRead] = b;
|
||||
bytesRead++;
|
||||
if ((bytesRead == 767) || (b == '\r')) {
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG(debugmsg.c_str());
|
||||
#endif
|
||||
LOG_DEBUG_GPS("%s", debugmsg.c_str());
|
||||
if (strnstr((char *)buffer, message, bytesRead) != nullptr) {
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG("Found: %s", message); // Log the found message
|
||||
#endif
|
||||
LOG_DEBUG_GPS("Found: %s", message); // Log the found message
|
||||
return GNSS_RESPONSE_OK;
|
||||
} else {
|
||||
bytesRead = 0;
|
||||
@@ -418,17 +418,13 @@ GPS_RESPONSE GPS::getACKCas(uint8_t class_id, uint8_t msg_id, uint32_t waitMilli
|
||||
|
||||
// Check for an ACK-ACK for the specified class and message id
|
||||
if ((msg_cls == 0x05) && (msg_msg_id == 0x01) && payload_cls == class_id && payload_msg == msg_id) {
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_INFO("Got ACK for class %02X message %02X in %dms", class_id, msg_id, millis() - startTime);
|
||||
#endif
|
||||
LOG_DEBUG_GPS("Got ACK for class %02X msg %02X in %dms", class_id, msg_id, millis() - startTime);
|
||||
return GNSS_RESPONSE_OK;
|
||||
}
|
||||
|
||||
// Check for an ACK-NACK for the specified class and message id
|
||||
if ((msg_cls == 0x05) && (msg_msg_id == 0x00) && payload_cls == class_id && payload_msg == msg_id) {
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_WARN("Got NACK for class %02X message %02X in %dms", class_id, msg_id, millis() - startTime);
|
||||
#endif
|
||||
LOG_DEBUG_GPS("Got NACK for class %02X msg %02X in %dms", class_id, msg_id, millis() - startTime);
|
||||
return GNSS_RESPONSE_NAK;
|
||||
}
|
||||
|
||||
@@ -450,7 +446,7 @@ GPS_RESPONSE GPS::getACK(uint8_t class_id, uint8_t msg_id, uint32_t waitMillis)
|
||||
uint32_t startTime = millis();
|
||||
const char frame_errors[] = "More than 100 frame errors";
|
||||
int sCounter = 0;
|
||||
#ifdef GPS_DEBUG
|
||||
#if GPS_DEBUG
|
||||
std::string debugmsg = "";
|
||||
#endif
|
||||
|
||||
@@ -467,9 +463,7 @@ GPS_RESPONSE GPS::getACK(uint8_t class_id, uint8_t msg_id, uint32_t waitMillis)
|
||||
|
||||
while (Throttle::isWithinTimespanMs(startTime, waitMillis)) {
|
||||
if (ack > 9) {
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_INFO("Got ACK for class %02X message %02X in %dms", class_id, msg_id, millis() - startTime);
|
||||
#endif
|
||||
LOG_DEBUG_GPS("Got ACK for class %02X msg %02X in %dms", class_id, msg_id, millis() - startTime);
|
||||
return GNSS_RESPONSE_OK; // ACK received
|
||||
}
|
||||
if (_serial_gps->available()) {
|
||||
@@ -477,36 +471,29 @@ GPS_RESPONSE GPS::getACK(uint8_t class_id, uint8_t msg_id, uint32_t waitMillis)
|
||||
if (b == frame_errors[sCounter]) {
|
||||
sCounter++;
|
||||
if (sCounter == 26) {
|
||||
#ifdef GPS_DEBUG
|
||||
|
||||
LOG_DEBUG(debugmsg.c_str());
|
||||
#endif
|
||||
LOG_DEBUG_GPS("%s", debugmsg.c_str());
|
||||
return GNSS_RESPONSE_FRAME_ERRORS;
|
||||
}
|
||||
} else {
|
||||
sCounter = 0;
|
||||
}
|
||||
#ifdef GPS_DEBUG
|
||||
#if GPS_DEBUG
|
||||
debugmsg += vformat("%02X", b);
|
||||
#endif
|
||||
if (b == buf[ack]) {
|
||||
ack++;
|
||||
} else {
|
||||
if (ack == 3 && b == 0x00) { // UBX-ACK-NAK message
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG(debugmsg.c_str());
|
||||
#endif
|
||||
LOG_WARN("Got NAK for class %02X message %02X", class_id, msg_id);
|
||||
LOG_DEBUG_GPS("%s", debugmsg.c_str());
|
||||
LOG_WARN("Got NAK for class %02X msg %02X", class_id, msg_id);
|
||||
return GNSS_RESPONSE_NAK; // NAK received
|
||||
}
|
||||
ack = 0; // Reset the acknowledgement counter
|
||||
}
|
||||
}
|
||||
}
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG(debugmsg.c_str());
|
||||
LOG_WARN("No response for class %02X message %02X", class_id, msg_id);
|
||||
#endif
|
||||
LOG_DEBUG_GPS("%s", debugmsg.c_str());
|
||||
LOG_DEBUG_GPS("No response for class %02X msg %02X", class_id, msg_id);
|
||||
return GNSS_RESPONSE_NONE; // No response received within timeout
|
||||
}
|
||||
|
||||
@@ -577,9 +564,7 @@ int GPS::getACK(uint8_t *buffer, uint16_t size, uint8_t requestedClass, uint8_t
|
||||
ubxFrameCounter = 0;
|
||||
} else {
|
||||
// return payload length
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_INFO("Got ACK for class %02X message %02X in %dms", requestedClass, requestedID, millis() - startTime);
|
||||
#endif
|
||||
LOG_DEBUG_GPS("Got ACK for class %02X msg %02X in %dms", requestedClass, requestedID, millis() - startTime);
|
||||
return needRead;
|
||||
}
|
||||
break;
|
||||
@@ -789,7 +774,7 @@ bool GPS::verifyCachedProbePresence()
|
||||
present = sawNmeaSentenceAtBaud(_serial_gps, 3000);
|
||||
}
|
||||
if (!present) {
|
||||
LOG_WARN("Cached GPS probe is stale (%s @ %d), clearing cache", cachedProbeModelName, cachedProbeBaud);
|
||||
LOG_WARN("Cached GPS probe stale (%s @ %d), clearing", cachedProbeModelName, cachedProbeBaud);
|
||||
clearProbeCache();
|
||||
return false;
|
||||
}
|
||||
@@ -843,7 +828,7 @@ bool GPS::setup()
|
||||
if (gnssModel != GNSS_MODEL_UNKNOWN) {
|
||||
detectedBaud = rareSerialSpeeds[speedSelect];
|
||||
} else if (currentStep == 0 && ++speedSelect == array_count(rareSerialSpeeds)) {
|
||||
LOG_WARN("Give up on GPS probe and set to %d", GPS_BAUDRATE);
|
||||
LOG_WARN("Give up GPS probe, set to %d", GPS_BAUDRATE);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -922,14 +907,14 @@ bool GPS::setup()
|
||||
msglen = makeCASPacket(0x06, 0x07, sizeof(_message_CAS_CFG_NAVX_CONF), _message_CAS_CFG_NAVX_CONF);
|
||||
_serial_gps->write(UBXscratch, msglen);
|
||||
if (getACKCas(0x06, 0x07, 250) != GNSS_RESPONSE_OK) {
|
||||
LOG_WARN("ATGM336H: Could not set Config");
|
||||
LOG_WARN("ATGM336H: Can't set Config");
|
||||
}
|
||||
|
||||
// Set the update frequency to 1Hz
|
||||
msglen = makeCASPacket(0x06, 0x04, sizeof(_message_CAS_CFG_RATE_1HZ), _message_CAS_CFG_RATE_1HZ);
|
||||
_serial_gps->write(UBXscratch, msglen);
|
||||
if (getACKCas(0x06, 0x04, 250) != GNSS_RESPONSE_OK) {
|
||||
LOG_WARN("ATGM336H: Could not set Update Frequency");
|
||||
LOG_WARN("ATGM336H: Can't set Update Frequency");
|
||||
}
|
||||
|
||||
// Set the NEMA output messages
|
||||
@@ -941,7 +926,7 @@ bool GPS::setup()
|
||||
msglen = makeCASPacket(0x06, 0x01, sizeof(cas_cfg_msg_packet), cas_cfg_msg_packet);
|
||||
_serial_gps->write(UBXscratch, msglen);
|
||||
if (getACKCas(0x06, 0x01, 250) != GNSS_RESPONSE_OK) {
|
||||
LOG_WARN("ATGM336H: Could not enable NMEA MSG: %d", fields[i]);
|
||||
LOG_WARN("ATGM336H: Can't enable NMEA MSG: %d", fields[i]);
|
||||
}
|
||||
}
|
||||
} else if (gnssModel == GNSS_MODEL_UC6580) {
|
||||
@@ -1009,9 +994,9 @@ bool GPS::setup()
|
||||
msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE), _message_SAVE);
|
||||
_serial_gps->write(UBXscratch, msglen);
|
||||
if (getACK(0x06, 0x09, 2000) != GNSS_RESPONSE_OK) {
|
||||
LOG_WARN("Unable to save GNSS module config");
|
||||
LOG_WARN("Can't save GNSS module config");
|
||||
} else {
|
||||
LOG_INFO("GNSS module config saved!");
|
||||
LOG_INFO("GNSS module config saved");
|
||||
}
|
||||
} else if (IS_ONE_OF(gnssModel, GNSS_MODEL_UBLOX7, GNSS_MODEL_UBLOX8, GNSS_MODEL_UBLOX9)) {
|
||||
if (gnssModel == GNSS_MODEL_UBLOX7) {
|
||||
@@ -1025,7 +1010,7 @@ bool GPS::setup()
|
||||
|
||||
if (getACK(0x06, 0x3e, 800) == GNSS_RESPONSE_NAK) {
|
||||
// It's not critical if the module doesn't acknowledge this configuration.
|
||||
LOG_DEBUG("reconfigure GNSS - defaults maintained. Is this module GPS-only?");
|
||||
LOG_DEBUG("reconfigure GNSS - defaults kept. GPS-only module?");
|
||||
} else {
|
||||
if (gnssModel == GNSS_MODEL_UBLOX7) {
|
||||
LOG_INFO("GPS+SBAS configured");
|
||||
@@ -1078,9 +1063,9 @@ bool GPS::setup()
|
||||
msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE), _message_SAVE);
|
||||
_serial_gps->write(UBXscratch, msglen);
|
||||
if (getACK(0x06, 0x09, 2000) != GNSS_RESPONSE_OK) {
|
||||
LOG_WARN("Unable to save GNSS module config");
|
||||
LOG_WARN("Can't save GNSS module config");
|
||||
} else {
|
||||
LOG_INFO("GNSS module configuration saved!");
|
||||
LOG_INFO("GNSS module config saved");
|
||||
}
|
||||
} else if (gnssModel == GNSS_MODEL_UBLOX10) {
|
||||
delay(1000);
|
||||
@@ -1126,9 +1111,9 @@ bool GPS::setup()
|
||||
msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE_10), _message_SAVE_10);
|
||||
_serial_gps->write(UBXscratch, msglen);
|
||||
if (getACK(0x06, 0x09, 2000) != GNSS_RESPONSE_OK) {
|
||||
LOG_WARN("Unable to save GNSS module config");
|
||||
LOG_WARN("Can't save GNSS module config");
|
||||
} else {
|
||||
LOG_INFO("GNSS module configuration saved!");
|
||||
LOG_INFO("GNSS module config saved");
|
||||
}
|
||||
} else if (gnssModel == GNSS_MODEL_CM121) {
|
||||
// only ask for RMC and GGA
|
||||
@@ -1159,7 +1144,7 @@ void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime)
|
||||
// Update the stored GPSPowerstate, and create local copies
|
||||
GPSPowerState oldState = powerState;
|
||||
powerState = newState;
|
||||
LOG_INFO("GPS power state move from %s to %s", getGPSPowerStateString(oldState), getGPSPowerStateString(newState));
|
||||
LOG_INFO("GPS power state %s -> %s", getGPSPowerStateString(oldState), getGPSPowerStateString(newState));
|
||||
|
||||
switch (newState) {
|
||||
case GPS_ACTIVE:
|
||||
@@ -1234,9 +1219,7 @@ void GPS::writePinEN(bool on)
|
||||
|
||||
// Write and log
|
||||
enablePin->set(on);
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG("Pin EN %s", on == HIGH ? "HI" : "LOW");
|
||||
#endif
|
||||
LOG_DEBUG_GPS("Pin EN %s", on == HIGH ? "HI" : "LOW");
|
||||
}
|
||||
|
||||
// Set the value of the STANDBY pin, if relevant
|
||||
@@ -1259,9 +1242,7 @@ void GPS::writePinStandby(bool standby)
|
||||
_serial_gps->write("$PMTK225,4*2F\r\n");
|
||||
}
|
||||
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG("Pin STANDBY %s", val == HIGH ? "HI" : "LOW");
|
||||
#endif
|
||||
LOG_DEBUG_GPS("Pin STANDBY %s", val == HIGH ? "HI" : "LOW");
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1272,9 +1253,7 @@ void GPS::writePinRFEN(bool on)
|
||||
bool val = on ? GPS_RF_EN_ACTIVE : !GPS_RF_EN_ACTIVE;
|
||||
pinMode(PIN_GPS_RF_EN, OUTPUT);
|
||||
digitalWrite(PIN_GPS_RF_EN, val);
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG("Pin RF EN %s", val == HIGH ? "HI" : "LOW");
|
||||
#endif
|
||||
LOG_DEBUG_GPS("Pin RF EN %s", val == HIGH ? "HI" : "LOW");
|
||||
#else
|
||||
(void)on;
|
||||
#endif
|
||||
@@ -1310,9 +1289,7 @@ void GPS::setPowerPMU(bool on)
|
||||
// t-beam v1.1 GNSS power channel
|
||||
on ? PMU->enablePowerOutput(XPOWERS_LDO3) : PMU->disablePowerOutput(XPOWERS_LDO3);
|
||||
}
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG("PMU %s", on ? "on" : "off");
|
||||
#endif
|
||||
LOG_DEBUG_GPS("PMU %s", on ? "on" : "off");
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1358,9 +1335,7 @@ void GPS::setPowerUBLOX(bool on, uint32_t sleepMs)
|
||||
|
||||
// Send the UBX packet
|
||||
gps->_serial_gps->write(gps->UBXscratch, msglen);
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG("UBLOX: sleep for %dmS", sleepMs);
|
||||
#endif
|
||||
LOG_DEBUG_GPS("UBLOX: sleep for %dmS", sleepMs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1417,12 +1392,8 @@ void GPS::down()
|
||||
#endif
|
||||
|
||||
if (softsleepSupported) {
|
||||
// How long does gps_update_interval need to be, for GPS_HARDSLEEP to become more efficient than
|
||||
// GPS_SOFTSLEEP? Heuristic equation. A compromise manually fitted to power observations from U-blox NEO-6M
|
||||
// and M10050 https://www.desmos.com/calculator/6gvjghoumr This is not particularly accurate, but probably an
|
||||
// improvement over a single, fixed threshold
|
||||
uint32_t hardsleepThreshold = (2750 * pow(predictedSearchDuration / 1000, 1.22));
|
||||
LOG_DEBUG("gps_update_interval >= %us needed to justify hardsleep", hardsleepThreshold / 1000);
|
||||
uint32_t hardsleepThreshold = gpsHardsleepThresholdMs(predictedSearchDuration / 1000);
|
||||
LOG_DEBUG("gps_update_interval >= %us needed for hardsleep", hardsleepThreshold / 1000);
|
||||
|
||||
// If update interval too short: softsleep (if supported by hardware)
|
||||
if (updateInterval < hardsleepThreshold) {
|
||||
@@ -1446,7 +1417,7 @@ void GPS::publishUpdate()
|
||||
LOG_DEBUG("Publish pos@%x:2, hasVal=%d, Sats=%d, GPSlock=%d", p.timestamp, hasValidLocation, p.sats_in_view, hasLock());
|
||||
|
||||
// Notify any status instances that are observing us
|
||||
const meshtastic::GPSStatus status = meshtastic::GPSStatus(hasValidLocation, isConnected(), isPowerSaving(), p);
|
||||
const meshtastic::GPSStatus status = meshtastic::GPSStatus(hasValidLocation, isConnected(), isPowerSaving(), p, gotTime);
|
||||
newStatus.notifyObservers(&status);
|
||||
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
|
||||
positionModule->handleNewPosition();
|
||||
@@ -1454,6 +1425,29 @@ void GPS::publishUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
/// Is a post-lock ephemeris hold currently in force? The `!= 0` is the "never armed" sentinel, which
|
||||
/// deadlinePassed() reads as passed for the first half of each wrap cycle and as ~24.8 days in the
|
||||
/// future for the second. No header: test_gps_fix_hold declares the prototypes itself.
|
||||
bool fixHoldInForce(uint32_t fixHoldEnds, uint32_t threadIntervalMs)
|
||||
{
|
||||
return fixHoldEnds != 0 && !Throttle::deadlinePassed(fixHoldEnds + threadIntervalMs);
|
||||
}
|
||||
|
||||
/// Did an armed hold just expire? `!= 0` guards against negating fixHoldInForce() alone, which would
|
||||
/// call an unarmed hold "expired" every cycle. No grace interval: the deadline itself is go-down time.
|
||||
bool holdJustExpired(uint32_t fixHoldEnds)
|
||||
{
|
||||
return fixHoldEnds != 0 && !fixHoldInForce(fixHoldEnds, 0);
|
||||
}
|
||||
|
||||
/// Should a post-lock ephemeris hold be (re-)armed this cycle? "No hold in force" fires often, since
|
||||
/// every publish clears the hold, including ones that don't put the receiver back to sleep.
|
||||
bool shouldArmFixHold(bool hasValidLocation, uint8_t prevFixQual, uint32_t fixHoldEnds, uint32_t threadIntervalMs)
|
||||
{
|
||||
// First lock of a cycle, first lock after the receiver was off, or nothing holding right now.
|
||||
return !hasValidLocation || prevFixQual == 0 || !fixHoldInForce(fixHoldEnds, threadIntervalMs);
|
||||
}
|
||||
|
||||
int32_t GPS::runOnce()
|
||||
{
|
||||
#if defined(SENSECAP_INDICATOR)
|
||||
@@ -1480,7 +1474,7 @@ int32_t GPS::runOnce()
|
||||
return currentDelay; // Setup failed, re-run in two seconds
|
||||
|
||||
if (gnssModel == GNSS_MODEL_UNKNOWN) {
|
||||
LOG_WARN("GPS not detected; marked not present for this boot");
|
||||
LOG_WARN("GPS not detected; not present this boot");
|
||||
return disable();
|
||||
}
|
||||
|
||||
@@ -1502,7 +1496,7 @@ int32_t GPS::runOnce()
|
||||
// gps_update_interval is faster than the position broadcast interval so there's a
|
||||
// fresh position ready when the device wants to broadcast one on the mesh.
|
||||
//
|
||||
// 1. Got a time for the first time --> set the time, don't publish.
|
||||
// 1. Got a time for the first time --> set the time, publish so the UI can show the time-only state.
|
||||
// 2. Got a lock for the first time
|
||||
// --> If gps_update_interval is <= 10s --> publishUpdate
|
||||
// --> Otherwise, hold for MIN(gps_update_interval - GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS, 20s)
|
||||
@@ -1536,12 +1530,17 @@ int32_t GPS::runOnce()
|
||||
// 1. Got a time for the first time this cycle
|
||||
if (!gotTime && lookForTime()) { // Note: we count on this && short-circuiting and not resetting the RTC time
|
||||
gotTime = true;
|
||||
// Publish immediately (rather than via the block below, which would clear fixHoldEnds) so the
|
||||
// time-only state reaches the UI without waiting for a location. Safe without a valid location:
|
||||
// PositionModule::handleNewPosition ignores invalid positions.
|
||||
shouldPublish = true;
|
||||
publishUpdate();
|
||||
}
|
||||
|
||||
// 2. Got a lock for the first time, or 3. Got a lock after turning back on
|
||||
bool gotLoc = lookForLocation();
|
||||
if (gotLoc) {
|
||||
#ifdef GPS_DEBUG
|
||||
#if GPS_DEBUG
|
||||
if (!hasValidLocation) { // declare that we have location ASAP
|
||||
LOG_DEBUG("hasValidLocation RISING EDGE");
|
||||
}
|
||||
@@ -1549,35 +1548,33 @@ int32_t GPS::runOnce()
|
||||
if (updateInterval <= GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS) {
|
||||
hasValidLocation = true;
|
||||
shouldPublish = true;
|
||||
} else if (!hasValidLocation || prev_fixQual == 0 || (fixHoldEnds + GPS_THREAD_INTERVAL) < millis()) {
|
||||
} else if (shouldArmFixHold(hasValidLocation, prev_fixQual, fixHoldEnds, GPS_THREAD_INTERVAL)) {
|
||||
hasValidLocation = true;
|
||||
// Hold for up to 20secs after getting a lock to download ephemeris etc
|
||||
uint32_t holdTime = updateInterval - GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS;
|
||||
if (holdTime > GPS_FIX_HOLD_MAX_MS)
|
||||
holdTime = GPS_FIX_HOLD_MAX_MS;
|
||||
fixHoldEnds = millis() + holdTime;
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG("Holding for %ums after lock", holdTime);
|
||||
#endif
|
||||
// Same clock the Throttle evaluation reads, and never the "no hold" sentinel.
|
||||
const uint32_t holdEnds = Time::getMillis() + holdTime;
|
||||
fixHoldEnds = holdEnds == 0 ? 1 : holdEnds;
|
||||
LOG_DEBUG_GPS("Holding for %ums after lock", holdTime);
|
||||
}
|
||||
}
|
||||
|
||||
bool tooLong = scheduling.searchedTooLong();
|
||||
if (tooLong && !gotLoc) {
|
||||
LOG_WARN("Couldn't publish a valid location: didn't get a GPS lock in time");
|
||||
LOG_WARN("Can't publish valid location: no GPS lock in time");
|
||||
// we didn't get a location during this ack window, therefore declare loss of lock
|
||||
if (hasValidLocation) {
|
||||
p = meshtastic_Position_init_default;
|
||||
hasValidLocation = false;
|
||||
shouldPublish = true;
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG("hasValidLocation FALLING EDGE");
|
||||
#endif
|
||||
LOG_DEBUG_GPS("hasValidLocation FALLING EDGE");
|
||||
}
|
||||
}
|
||||
|
||||
// Hold has expired , Search time has expired, we got a time only, or we never needed to hold.
|
||||
bool holdExpired = (fixHoldEnds != 0 && millis() > fixHoldEnds);
|
||||
bool holdExpired = holdJustExpired(fixHoldEnds);
|
||||
if (shouldPublish || tooLong || holdExpired) {
|
||||
if (gotTime && hasValidLocation) {
|
||||
shouldPublish = true;
|
||||
@@ -1592,9 +1589,9 @@ int32_t GPS::runOnce()
|
||||
down();
|
||||
}
|
||||
|
||||
#ifdef GPS_DEBUG
|
||||
#if GPS_DEBUG
|
||||
} else if (fixHoldEnds != 0) {
|
||||
LOG_DEBUG("Holding for GPS data download: %d ms (numSats=%d)", fixHoldEnds - millis(), p.sats_in_view);
|
||||
LOG_DEBUG("Holding for GPS data download: %d ms (numSats=%d)", fixHoldEnds - Time::getMillis(), p.sats_in_view);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -1623,7 +1620,7 @@ void GPS::clearBuffer()
|
||||
/// Prepare the GPS for the cpu entering deep or light sleep, expect to be gone for at least 100s of msecs
|
||||
int GPS::prepareDeepSleep(void *unused)
|
||||
{
|
||||
LOG_INFO("GPS deep sleep!");
|
||||
LOG_INFO("GPS deep sleep");
|
||||
disable();
|
||||
return 0;
|
||||
}
|
||||
@@ -1819,7 +1816,6 @@ GnssModel_t GPS::probe(int serialSpeed)
|
||||
break;
|
||||
}
|
||||
|
||||
LOG_DEBUG("Module Info : ");
|
||||
LOG_DEBUG("Soft version: %s", ublox_info.swVersion);
|
||||
LOG_DEBUG("Hard version: %s", ublox_info.hwVersion);
|
||||
LOG_DEBUG("Extensions:%d", ublox_info.extensionNo);
|
||||
@@ -1899,27 +1895,21 @@ GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector<ChipI
|
||||
// check if we can see our chips
|
||||
for (const auto &chipInfo : responseMap) {
|
||||
if (strstr(response.get(), chipInfo.detectionString.c_str()) != nullptr) {
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG(response.get());
|
||||
#endif
|
||||
LOG_DEBUG_GPS("%s", response.get());
|
||||
LOG_INFO("%s detected", chipInfo.chipName.c_str());
|
||||
return chipInfo.driver;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n') {
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG(response.get());
|
||||
#endif
|
||||
LOG_DEBUG_GPS("%s", response.get());
|
||||
// Reset the response buffer for the next potential message
|
||||
responseLen = 0;
|
||||
response[0] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG(response.get());
|
||||
#endif
|
||||
LOG_DEBUG_GPS("%s", response.get());
|
||||
return GNSS_MODEL_UNKNOWN; // Return unknown on timeout
|
||||
}
|
||||
|
||||
@@ -2025,7 +2015,7 @@ std::unique_ptr<GPS> GPS::createGps()
|
||||
#endif
|
||||
|
||||
#if defined(SENSECAP_INDICATOR)
|
||||
LOG_DEBUG("Use the RP2040 tunnel for GPS, no local pins");
|
||||
LOG_DEBUG("Use RP2040 tunnel for GPS, no local pins");
|
||||
#else
|
||||
LOG_DEBUG("Use GPIO%d for GPS RX", new_gps->rx_gpio);
|
||||
LOG_DEBUG("Use GPIO%d for GPS TX", new_gps->tx_gpio);
|
||||
@@ -2120,10 +2110,10 @@ bool GPS::lookForLocation()
|
||||
#ifndef TINYGPS_OPTION_NO_STATISTICS
|
||||
if (reader.failedChecksum() > lastChecksumFailCount) {
|
||||
// In a GPS_DEBUG build we want to log all of these. In production, we only care if there are many of them.
|
||||
#ifndef GPS_DEBUG
|
||||
#if !GPS_DEBUG
|
||||
if (reader.failedChecksum() > 4)
|
||||
#endif
|
||||
LOG_WARN("%u new GPS checksum failures, for a total of %u", reader.failedChecksum() - lastChecksumFailCount,
|
||||
LOG_WARN("%u new GPS checksum failures, total %u", reader.failedChecksum() - lastChecksumFailCount,
|
||||
reader.failedChecksum());
|
||||
lastChecksumFailCount = reader.failedChecksum();
|
||||
}
|
||||
@@ -2137,7 +2127,7 @@ bool GPS::lookForLocation()
|
||||
if (!hasLock())
|
||||
return false;
|
||||
|
||||
#ifdef GPS_DEBUG
|
||||
#if GPS_DEBUG
|
||||
LOG_DEBUG("AGE: LOC=%d FIX=%d DATE=%d TIME=%d", reader.location.age(),
|
||||
#ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS
|
||||
gsafixtype.age(),
|
||||
@@ -2159,7 +2149,7 @@ bool GPS::lookForLocation()
|
||||
(gsafixtype.age() < GPS_SOL_EXPIRY_MS) &&
|
||||
#endif
|
||||
(reader.time.age() < GPS_SOL_EXPIRY_MS) && (reader.date.age() < GPS_SOL_EXPIRY_MS))) {
|
||||
LOG_WARN("SOME data is TOO OLD: LOC %u, TIME %u, DATE %u", reader.location.age(), reader.time.age(), reader.date.age());
|
||||
LOG_WARN("SOME data TOO OLD: LOC %u, TIME %u, DATE %u", reader.location.age(), reader.time.age(), reader.date.age());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2168,15 +2158,11 @@ bool GPS::lookForLocation()
|
||||
|
||||
// Bail out EARLY to avoid overwriting previous good data (like #857)
|
||||
if (toDegInt(loc.lat) > 900000000) {
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG("Bail out EARLY on LAT %i", toDegInt(loc.lat));
|
||||
#endif
|
||||
LOG_DEBUG_GPS("Bail out EARLY on LAT %i", toDegInt(loc.lat));
|
||||
return false;
|
||||
}
|
||||
if (toDegInt(loc.lng) > 1800000000) {
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG("Bail out EARLY on LNG %i", toDegInt(loc.lng));
|
||||
#endif
|
||||
LOG_DEBUG_GPS("Bail out EARLY on LNG %i", toDegInt(loc.lng));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2261,7 +2247,7 @@ bool GPS::whileActive()
|
||||
{
|
||||
unsigned int charsInBuf = 0;
|
||||
bool isValid = false;
|
||||
#ifdef GPS_DEBUG
|
||||
#if GPS_DEBUG
|
||||
std::string debugmsg = "";
|
||||
#endif
|
||||
if (powerState != GPS_ACTIVE) {
|
||||
@@ -2270,7 +2256,7 @@ bool GPS::whileActive()
|
||||
}
|
||||
#ifdef SERIAL_BUFFER_SIZE
|
||||
if (_serial_gps->available() >= SERIAL_BUFFER_SIZE - 1) {
|
||||
LOG_WARN("GPS Buffer full with %u bytes waiting. Flush to avoid corruption", _serial_gps->available());
|
||||
LOG_WARN("GPS Buffer full (%u bytes). Flush to avoid corruption", _serial_gps->available());
|
||||
clearBuffer();
|
||||
}
|
||||
#endif
|
||||
@@ -2278,7 +2264,7 @@ bool GPS::whileActive()
|
||||
while (_serial_gps->available() > 0) {
|
||||
int c = _serial_gps->read();
|
||||
UBXscratch[charsInBuf] = c;
|
||||
#ifdef GPS_DEBUG
|
||||
#if GPS_DEBUG
|
||||
debugmsg += vformat("%c", (c >= 32 && c <= 126) ? c : '.');
|
||||
#endif
|
||||
isValid |= reader.encode(c);
|
||||
@@ -2291,9 +2277,9 @@ bool GPS::whileActive()
|
||||
charsInBuf++;
|
||||
}
|
||||
}
|
||||
#ifdef GPS_DEBUG
|
||||
#if GPS_DEBUG
|
||||
if (debugmsg != "") {
|
||||
LOG_DEBUG(debugmsg.c_str());
|
||||
LOG_DEBUG("%s", debugmsg.c_str());
|
||||
}
|
||||
#endif
|
||||
return isValid;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "DebugConfiguration.h"
|
||||
|
||||
// GPS_DEBUG=1 enables verbose GNSS diagnostics (probe/ACK byte dumps, pin states, NMEA ages).
|
||||
// Costs no flash when off. Genuine LOG_WARN anomalies stay unconditional.
|
||||
#ifndef GPS_DEBUG
|
||||
#define GPS_DEBUG 0
|
||||
#endif
|
||||
#if GPS_DEBUG
|
||||
#define LOG_DEBUG_GPS(...) LOG_DEBUG(__VA_ARGS__)
|
||||
#else
|
||||
#define LOG_DEBUG_GPS(...) ((void)0)
|
||||
#endif
|
||||
@@ -2,6 +2,31 @@
|
||||
|
||||
#include "Default.h"
|
||||
|
||||
// Sampled from the original `2750 * seconds^1.22` curve. Interpolation tracks it within 0.6% for
|
||||
// inputs >=10s and 1.7% below that; the 1s/2s/3s points keep the convex first segment from
|
||||
// overshooting (a 0s-to-5s chord reads 42% high at 1s).
|
||||
static constexpr uint32_t kThresholdCurveSecs[] = {0, 1, 2, 3, 5, 10, 15, 20, 30, 45, 60, 90, 120, 180, 240, 300, 450, 600, 900};
|
||||
static constexpr uint32_t kThresholdCurveMs[] = {0, 2750, 6406, 10506, 19592, 45639, 74845,
|
||||
106314, 174350, 285925, 406141, 666053, 946093, 1551548,
|
||||
2203893, 2893481, 4745172, 6740269, 11053722};
|
||||
static constexpr size_t kThresholdCurvePoints = sizeof(kThresholdCurveSecs) / sizeof(kThresholdCurveSecs[0]);
|
||||
|
||||
// How long does gps_update_interval need to be, for GPS_HARDSLEEP to become more efficient than
|
||||
// GPS_SOFTSLEEP? Avoids pow() so this heuristic doesn't pull double-precision libm into the image.
|
||||
uint32_t gpsHardsleepThresholdMs(uint32_t predictedSearchSecs)
|
||||
{
|
||||
if (predictedSearchSecs >= kThresholdCurveSecs[kThresholdCurvePoints - 1])
|
||||
return kThresholdCurveMs[kThresholdCurvePoints - 1];
|
||||
|
||||
size_t i = 1;
|
||||
while (kThresholdCurveSecs[i] < predictedSearchSecs)
|
||||
i++;
|
||||
|
||||
uint32_t x0 = kThresholdCurveSecs[i - 1], x1 = kThresholdCurveSecs[i];
|
||||
uint32_t y0 = kThresholdCurveMs[i - 1], y1 = kThresholdCurveMs[i];
|
||||
return y0 + (uint32_t)((uint64_t)(y1 - y0) * (predictedSearchSecs - x0) / (x1 - x0));
|
||||
}
|
||||
|
||||
// Mark the time when searching for GPS position begins
|
||||
void GPSUpdateScheduling::informSearching()
|
||||
{
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
// Approximates the GPS_HARDSLEEP/GPS_SOFTSLEEP crossover curve without pow(); see .cpp for the
|
||||
// sampled reference values it interpolates between.
|
||||
uint32_t gpsHardsleepThresholdMs(uint32_t predictedSearchSecs);
|
||||
|
||||
// Encapsulates code responsible for the timing of GPS updates
|
||||
class GPSUpdateScheduling
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "GeoCoord.h"
|
||||
#include "configuration.h"
|
||||
#include <cmath>
|
||||
|
||||
// Narrow a UTM meter value to its unsigned field, clamping non-finite/negative/oversized inputs: an
|
||||
@@ -433,6 +434,43 @@ void GeoCoord::convertWGS84ToOSGB36(const double lat, const double lon, double &
|
||||
//(airyA*airyA/(airyA / sqrt(1 - airyEcc*sin(osgb.latitude)*sin(osgb.latitude)))); // Not used, no OSTN data
|
||||
}
|
||||
|
||||
#if MESHTASTIC_TRIG_APPROX
|
||||
// cos(x) minimax approx for x in [-pi/2, pi/2] ("cos_52"): https://www.ganssle.com/approx.htm
|
||||
static double cosLatitudeApprox(double latRad)
|
||||
{
|
||||
constexpr double c1 = 0.9999932946, c2 = -0.4999124376, c3 = 0.0414877472, c4 = -0.0012712095;
|
||||
double x2 = latRad * latRad;
|
||||
return c1 + x2 * (c2 + x2 * (c3 + c4 * x2));
|
||||
}
|
||||
|
||||
/// Approximate distance in meters via equirectangular projection (not exact spherical trig).
|
||||
/// <1% error to ~500km, degrading near the poles at long range (see test_geocoord_distance).
|
||||
float GeoCoord::latLongToMeter(double lat_a, double lng_a, double lat_b, double lng_b)
|
||||
{
|
||||
// Don't do math if the points are the same
|
||||
if (lat_a == lat_b && lng_a == lng_b)
|
||||
return 0.0;
|
||||
|
||||
double a1 = lat_a / DEG_CONVERT;
|
||||
double a2 = lng_a / DEG_CONVERT;
|
||||
double b1 = lat_b / DEG_CONVERT;
|
||||
double b2 = lng_b / DEG_CONVERT;
|
||||
|
||||
double meanLat = (a1 + b1) / 2;
|
||||
double dLng = b2 - a2;
|
||||
// Wrap to [-PI, PI]: unlike cos()/sin(), a raw longitude difference doesn't handle points that
|
||||
// straddle the antimeridian (e.g. 179.9 and -179.9 are ~0.2 degrees apart, not ~360).
|
||||
if (dLng > PI)
|
||||
dLng -= 2 * PI;
|
||||
else if (dLng < -PI)
|
||||
dLng += 2 * PI;
|
||||
double x = dLng * cosLatitudeApprox(meanLat);
|
||||
double y = b1 - a1;
|
||||
double tt = sqrt(x * x + y * y);
|
||||
|
||||
return (float)(6366000 * tt);
|
||||
}
|
||||
#else
|
||||
/// Ported from my old java code, returns distance in meters along the globe
|
||||
/// surface (by Haversine formula)
|
||||
float GeoCoord::latLongToMeter(double lat_a, double lng_a, double lat_b, double lng_b)
|
||||
@@ -456,6 +494,7 @@ float GeoCoord::latLongToMeter(double lat_a, double lng_a, double lat_b, double
|
||||
|
||||
return (float)(6366000 * tt);
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Computes the bearing in degrees between two points on Earth. Ported from my
|
||||
|
||||
+66
-57
@@ -1,9 +1,12 @@
|
||||
#include "gps/RTC.h"
|
||||
#include "UptimeClock.h"
|
||||
#include "configuration.h"
|
||||
#include "detect/ScanI2C.h"
|
||||
#include "detect/ScanI2CTwoWire.h"
|
||||
#include "gps/GPSLog.h"
|
||||
#include "main.h"
|
||||
#include "mesh/MeshService.h"
|
||||
#include "mesh/NodeDB.h"
|
||||
#include "modules/NodeInfoModule.h"
|
||||
#include <Throttle.h>
|
||||
#include <sys/time.h>
|
||||
@@ -22,12 +25,15 @@ static const uint32_t TIME_VALIDATION_WARNING_INTERVAL_MS = 15000; // 15 seconds
|
||||
static void onTimeSourceQualityChanged(RTCQuality oldQuality, RTCQuality newQuality)
|
||||
{
|
||||
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), recheck NodeInfo", RtcName(oldQuality), RtcName(newQuality));
|
||||
nodeInfoModule->triggerImmediateNodeInfoCheck();
|
||||
}
|
||||
if (oldQuality < RTCQualityFromNet && newQuality >= RTCQualityFromNet && service) {
|
||||
if (oldQuality < RTCQualityFromNet && newQuality >= RTCQualityFromNet) {
|
||||
LOG_DEBUG("RTC net quality reached (%s -> %s), reconciling rx_time", RtcName(oldQuality), RtcName(newQuality));
|
||||
service->reconcilePendingRxTimes();
|
||||
if (service)
|
||||
service->reconcilePendingRxTimes();
|
||||
if (nodeDB)
|
||||
nodeDB->backfillHeardAt();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +43,9 @@ RTCQuality getRTCQuality()
|
||||
}
|
||||
|
||||
// stuff that really should be in in the instance instead...
|
||||
static uint32_t
|
||||
timeStartMsec; // Once we have a GPS lock, this is where we hold the initial msec clock that corresponds to that time
|
||||
// The Time::getMillisMonotonic() instant corresponding to zeroOffsetSecs. 64-bit so getTime()'s
|
||||
// elapsed term cannot wrap: a 32-bit anchor walks the wall clock back 49.7 days per millis() cycle.
|
||||
static uint64_t timeStartMs64;
|
||||
static uint64_t zeroOffsetSecs; // GPS based time in secs since 1970 - only updated once on initial lock
|
||||
|
||||
#ifdef PIO_UNIT_TESTING
|
||||
@@ -70,14 +77,14 @@ static struct timeval mockSystemTime = {};
|
||||
{
|
||||
struct timeval tv;
|
||||
if (readSystemTime(&tv)) {
|
||||
uint32_t now = millis();
|
||||
const uint64_t now = Time::getMillisMonotonic();
|
||||
uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
|
||||
if (currentQuality == RTCQualityNone) {
|
||||
LOG_DEBUG("Seed time from system clock: %lu", (unsigned long)printableEpoch);
|
||||
timeStartMsec = now;
|
||||
timeStartMs64 = now;
|
||||
zeroOffsetSecs = tv.tv_sec;
|
||||
} else {
|
||||
LOG_DEBUG("Ignore system clock fallback (%lu); current RTC quality is %s", (unsigned long)printableEpoch,
|
||||
LOG_DEBUG("Ignore system clock fallback (%lu); RTC quality is %s", (unsigned long)printableEpoch,
|
||||
RtcName(currentQuality));
|
||||
}
|
||||
return RTCSetResultSuccess;
|
||||
@@ -100,7 +107,7 @@ RTCSetResult readFromRTC()
|
||||
[[maybe_unused]] struct timeval tv; /* btw settimeofday() is helpful here too*/
|
||||
#ifdef RV3028_RTC
|
||||
if (rtc_found.address == RV3028_RTC) {
|
||||
uint32_t now = millis();
|
||||
const uint64_t now = Time::getMillisMonotonic();
|
||||
Melopero_RV3028 rtc;
|
||||
#if WIRE_INTERFACES_COUNT == 2
|
||||
rtc.initI2C(*ScanI2CTwoWire::fetchI2CBus(rtc_found));
|
||||
@@ -121,24 +128,24 @@ RTCSetResult readFromRTC()
|
||||
#ifdef BUILD_EPOCH
|
||||
if (tv.tv_sec < BUILD_EPOCH) {
|
||||
if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
|
||||
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
|
||||
LOG_WARN("Ignore time (%ld) before build epoch (%ld)", printableEpoch, BUILD_EPOCH);
|
||||
}
|
||||
return RTCSetResultInvalidTime;
|
||||
}
|
||||
#endif
|
||||
|
||||
LOG_DEBUG("Read RTC time from RV3028 getTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1,
|
||||
t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch);
|
||||
LOG_DEBUG_GPS("RTC time from RV3028 getTime: %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1,
|
||||
t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch);
|
||||
if (currentQuality == RTCQualityNone) {
|
||||
RTCQuality oldQuality = currentQuality;
|
||||
timeStartMsec = now;
|
||||
timeStartMs64 = now;
|
||||
zeroOffsetSecs = tv.tv_sec;
|
||||
currentQuality = RTCQualityDevice;
|
||||
onTimeSourceQualityChanged(oldQuality, currentQuality);
|
||||
}
|
||||
return RTCSetResultSuccess;
|
||||
} else {
|
||||
LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address);
|
||||
LOG_WARN("RTC read: not found (addr 0x%02X)", rtc_found.address);
|
||||
}
|
||||
#elif defined(PCF8563_RTC) || defined(PCF85063_RTC)
|
||||
#if defined(PCF8563_RTC)
|
||||
@@ -149,7 +156,7 @@ RTCSetResult readFromRTC()
|
||||
SensorPCF85063 rtc;
|
||||
|
||||
#endif
|
||||
uint32_t now = millis();
|
||||
const uint64_t now = Time::getMillisMonotonic();
|
||||
|
||||
#if WIRE_INTERFACES_COUNT == 2
|
||||
rtc.begin(*ScanI2CTwoWire::fetchI2CBus(rtc_found));
|
||||
@@ -166,29 +173,29 @@ RTCSetResult readFromRTC()
|
||||
#ifdef BUILD_EPOCH
|
||||
if (tv.tv_sec < BUILD_EPOCH) {
|
||||
if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
|
||||
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
|
||||
LOG_WARN("Ignore time (%ld) before build epoch (%ld)", printableEpoch, BUILD_EPOCH);
|
||||
lastTimeValidationWarning = millis();
|
||||
}
|
||||
return RTCSetResultInvalidTime;
|
||||
}
|
||||
#endif
|
||||
|
||||
LOG_DEBUG("Read RTC time from %s getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", rtc.getChipName(), t.tm_year + 1900,
|
||||
t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch);
|
||||
LOG_DEBUG_GPS("RTC time from %s getDateTime: %02d-%02d-%02d %02d:%02d:%02d (%ld)", rtc.getChipName(), t.tm_year + 1900,
|
||||
t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch);
|
||||
if (currentQuality == RTCQualityNone) {
|
||||
RTCQuality oldQuality = currentQuality;
|
||||
timeStartMsec = now;
|
||||
timeStartMs64 = now;
|
||||
zeroOffsetSecs = tv.tv_sec;
|
||||
currentQuality = RTCQualityDevice;
|
||||
onTimeSourceQualityChanged(oldQuality, currentQuality);
|
||||
}
|
||||
return RTCSetResultSuccess;
|
||||
} else {
|
||||
LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address);
|
||||
LOG_WARN("RTC read: not found (addr 0x%02X)", rtc_found.address);
|
||||
}
|
||||
#elif defined(RX8130CE_RTC)
|
||||
if (rtc_found.address == RX8130CE_RTC) {
|
||||
uint32_t now = millis();
|
||||
const uint64_t now = Time::getMillisMonotonic();
|
||||
#ifdef MUZI_BASE
|
||||
ArtronShop_RX8130CE rtc(&Wire1);
|
||||
#else
|
||||
@@ -200,12 +207,12 @@ RTCSetResult readFromRTC()
|
||||
tv.tv_usec = 0;
|
||||
|
||||
uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
|
||||
LOG_DEBUG("Read RTC time from RX8130CE getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900,
|
||||
t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch);
|
||||
LOG_DEBUG_GPS("RTC time from RX8130CE getDateTime: %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900,
|
||||
t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch);
|
||||
#ifdef BUILD_EPOCH
|
||||
if (tv.tv_sec < BUILD_EPOCH) {
|
||||
if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
|
||||
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
|
||||
LOG_WARN("Ignore time (%ld) before build epoch (%ld)", printableEpoch, BUILD_EPOCH);
|
||||
lastTimeValidationWarning = millis();
|
||||
}
|
||||
return RTCSetResultInvalidTime;
|
||||
@@ -213,7 +220,7 @@ RTCSetResult readFromRTC()
|
||||
#endif
|
||||
if (currentQuality == RTCQualityNone) {
|
||||
RTCQuality oldQuality = currentQuality;
|
||||
timeStartMsec = now;
|
||||
timeStartMs64 = now;
|
||||
zeroOffsetSecs = tv.tv_sec;
|
||||
currentQuality = RTCQualityDevice;
|
||||
onTimeSourceQualityChanged(oldQuality, currentQuality);
|
||||
@@ -223,14 +230,14 @@ RTCSetResult readFromRTC()
|
||||
}
|
||||
#elif HAS_LSE
|
||||
if (stm32wlRtcAvailable()) {
|
||||
uint32_t now = millis();
|
||||
const uint64_t now = Time::getMillisMonotonic();
|
||||
tv.tv_sec = STM32RTC::getInstance().getEpoch();
|
||||
tv.tv_usec = 0;
|
||||
uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
|
||||
#ifdef BUILD_EPOCH
|
||||
if (tv.tv_sec < BUILD_EPOCH) {
|
||||
if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
|
||||
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
|
||||
LOG_WARN("Ignore time (%ld) before build epoch (%ld)", printableEpoch, BUILD_EPOCH);
|
||||
lastTimeValidationWarning = millis();
|
||||
}
|
||||
return RTCSetResultInvalidTime;
|
||||
@@ -238,7 +245,7 @@ RTCSetResult readFromRTC()
|
||||
#endif
|
||||
if (currentQuality == RTCQualityNone) {
|
||||
RTCQuality oldQuality = currentQuality;
|
||||
timeStartMsec = now;
|
||||
timeStartMs64 = now;
|
||||
zeroOffsetSecs = tv.tv_sec;
|
||||
currentQuality = RTCQualityDevice;
|
||||
onTimeSourceQualityChanged(oldQuality, currentQuality);
|
||||
@@ -263,12 +270,13 @@ RTCSetResult readFromRTC()
|
||||
RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate)
|
||||
{
|
||||
static uint32_t lastSetMsec = 0;
|
||||
uint32_t now = millis();
|
||||
const uint64_t now64 = Time::getMillisMonotonic();
|
||||
const uint32_t now = (uint32_t)now64; // low word == getMillis(); fine for the Throttle-checked stamps below
|
||||
uint32_t printableEpoch = tv->tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
|
||||
#ifdef BUILD_EPOCH
|
||||
if (tv->tv_sec < BUILD_EPOCH) {
|
||||
if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
|
||||
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
|
||||
LOG_WARN("Ignore time (%ld) before build epoch (%ld)", printableEpoch, BUILD_EPOCH);
|
||||
lastTimeValidationWarning = millis();
|
||||
}
|
||||
return RTCSetResultInvalidTime;
|
||||
@@ -277,8 +285,8 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
|
||||
// Calculate max allowed time safely to avoid overflow in logging
|
||||
uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS;
|
||||
uint32_t maxAllowedPrintable = (maxAllowedTime > UINT32_MAX) ? UINT32_MAX : (uint32_t)maxAllowedTime;
|
||||
LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch,
|
||||
(uint32_t)BUILD_EPOCH, maxAllowedPrintable);
|
||||
LOG_WARN("Ignore time (%ld) too far in future (build epoch: %ld, max: %ld)", printableEpoch, (uint32_t)BUILD_EPOCH,
|
||||
maxAllowedPrintable);
|
||||
lastTimeValidationWarning = millis();
|
||||
}
|
||||
return RTCSetResultInvalidTime;
|
||||
@@ -288,21 +296,20 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
|
||||
bool shouldSet;
|
||||
if (forceUpdate) {
|
||||
shouldSet = true;
|
||||
LOG_DEBUG("Override current RTC quality (%s) with incoming time of RTC quality of %s", RtcName(currentQuality),
|
||||
RtcName(q));
|
||||
LOG_DEBUG("Override RTC quality (%s) with incoming quality %s", RtcName(currentQuality), RtcName(q));
|
||||
} else if (q > currentQuality) {
|
||||
shouldSet = true;
|
||||
LOG_DEBUG("Upgrade time to quality %s", RtcName(q));
|
||||
} else if (q == RTCQualityGPS) {
|
||||
shouldSet = true;
|
||||
LOG_DEBUG("Reapply GPS time: %ld secs", printableEpoch);
|
||||
LOG_DEBUG_GPS("Reapply GPS time: %ld secs", printableEpoch);
|
||||
} else if (q == RTCQualityNTP && !Throttle::isWithinTimespanMs(lastSetMsec, (30 * 60 * 1000UL))) {
|
||||
// Every 30 minutes we will slam in a new NTP or Phone GPS / NTP time, to correct for local RTC clock drift
|
||||
shouldSet = true;
|
||||
LOG_DEBUG("Reapply external time to correct clock drift %ld secs", printableEpoch);
|
||||
LOG_DEBUG_GPS("Reapply external time to fix clock drift %ld secs", printableEpoch);
|
||||
} else {
|
||||
shouldSet = false;
|
||||
LOG_DEBUG("Current RTC quality: %s. Ignore time of RTC quality of %s", RtcName(currentQuality), RtcName(q));
|
||||
LOG_DEBUG_GPS("RTC quality: %s. Ignore time of quality %s", RtcName(currentQuality), RtcName(q));
|
||||
}
|
||||
|
||||
if (shouldSet) {
|
||||
@@ -314,7 +321,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
|
||||
}
|
||||
|
||||
// This delta value works on all platforms
|
||||
timeStartMsec = now;
|
||||
timeStartMs64 = now64;
|
||||
zeroOffsetSecs = tv->tv_sec;
|
||||
// If this platform has a settable RTC, set it
|
||||
#ifdef RV3028_RTC
|
||||
@@ -328,12 +335,12 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
|
||||
// tv_sec is a long, which is not time_t everywhere: on Windows
|
||||
// time_t is 64-bit while long is 32-bit. Copy before taking &.
|
||||
time_t setSecs = tv->tv_sec;
|
||||
tm *t = gmtime(&setSecs);
|
||||
const tm *t = gmtime(&setSecs);
|
||||
rtc.setTime(t->tm_year + 1900, t->tm_mon + 1, t->tm_wday, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);
|
||||
LOG_DEBUG("RV3028_RTC setTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,
|
||||
t->tm_hour, t->tm_min, t->tm_sec, printableEpoch);
|
||||
LOG_DEBUG_GPS("RV3028_RTC setTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,
|
||||
t->tm_hour, t->tm_min, t->tm_sec, printableEpoch);
|
||||
} else {
|
||||
LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address);
|
||||
LOG_WARN("RTC set: not found (addr 0x%02X)", rtc_found.address);
|
||||
}
|
||||
#elif defined(PCF8563_RTC) || defined(PCF85063_RTC)
|
||||
#if defined(PCF8563_RTC)
|
||||
@@ -353,12 +360,12 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
|
||||
// tv_sec is a long, which is not time_t everywhere: on Windows
|
||||
// time_t is 64-bit while long is 32-bit. Copy before taking &.
|
||||
time_t setSecs = tv->tv_sec;
|
||||
tm *t = gmtime(&setSecs);
|
||||
const tm *t = gmtime(&setSecs);
|
||||
rtc.setDateTime(*t);
|
||||
LOG_DEBUG("%s setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", rtc.getChipName(), t->tm_year + 1900, t->tm_mon + 1,
|
||||
t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, printableEpoch);
|
||||
LOG_DEBUG_GPS("%s setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", rtc.getChipName(), t->tm_year + 1900,
|
||||
t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, printableEpoch);
|
||||
} else {
|
||||
LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address);
|
||||
LOG_WARN("RTC set: not found (addr 0x%02X)", rtc_found.address);
|
||||
}
|
||||
#elif defined(RX8130CE_RTC)
|
||||
if (rtc_found.address == RX8130CE_RTC) {
|
||||
@@ -370,12 +377,12 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
|
||||
// tv_sec is a long, which is not time_t everywhere: on Windows
|
||||
// time_t is 64-bit while long is 32-bit. Copy before taking &.
|
||||
time_t setSecs = tv->tv_sec;
|
||||
tm *t = gmtime(&setSecs);
|
||||
const tm *t = gmtime(&setSecs);
|
||||
if (rtc.setTime(*t)) {
|
||||
LOG_DEBUG("RX8130CE setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1,
|
||||
t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, printableEpoch);
|
||||
LOG_DEBUG_GPS("RX8130CE setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1,
|
||||
t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, printableEpoch);
|
||||
} else {
|
||||
LOG_WARN("Failed to set time for RX8130CE");
|
||||
LOG_WARN("RX8130CE set time failed");
|
||||
}
|
||||
}
|
||||
#elif HAS_LSE
|
||||
@@ -435,7 +442,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct tm &t)
|
||||
#ifdef BUILD_EPOCH
|
||||
if (tv.tv_sec < BUILD_EPOCH) {
|
||||
if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
|
||||
LOG_WARN("Ignore time (%lu) before build epoch (%lu)!", printableEpoch, BUILD_EPOCH);
|
||||
LOG_WARN("Ignore time (%lu) before build epoch (%lu)", printableEpoch, BUILD_EPOCH);
|
||||
lastTimeValidationWarning = millis();
|
||||
}
|
||||
return RTCSetResultInvalidTime;
|
||||
@@ -444,8 +451,8 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct tm &t)
|
||||
// Calculate max allowed time safely to avoid overflow in logging
|
||||
uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS;
|
||||
uint32_t maxAllowedPrintable = (maxAllowedTime > UINT32_MAX) ? UINT32_MAX : (uint32_t)maxAllowedTime;
|
||||
LOG_WARN("Ignore time (%lu) too far in the future (build epoch: %lu, max allowed: %lu)!", printableEpoch,
|
||||
(uint32_t)BUILD_EPOCH, maxAllowedPrintable);
|
||||
LOG_WARN("Ignore time (%lu) too far in future (build epoch: %lu, max: %lu)", printableEpoch, (uint32_t)BUILD_EPOCH,
|
||||
maxAllowedPrintable);
|
||||
lastTimeValidationWarning = millis();
|
||||
}
|
||||
return RTCSetResultInvalidTime;
|
||||
@@ -486,10 +493,12 @@ int32_t getTZOffset()
|
||||
*/
|
||||
uint32_t getTime(bool local)
|
||||
{
|
||||
// Both terms are 64-bit monotonic, so the elapsed time cannot wrap - see timeStartMs64.
|
||||
const uint64_t elapsedSecs = (Time::getMillisMonotonic() - timeStartMs64) / 1000;
|
||||
if (local) {
|
||||
return (((uint32_t)millis() - timeStartMsec) / 1000) + zeroOffsetSecs + getTZOffset();
|
||||
return elapsedSecs + zeroOffsetSecs + getTZOffset();
|
||||
} else {
|
||||
return (((uint32_t)millis() - timeStartMsec) / 1000) + zeroOffsetSecs;
|
||||
return elapsedSecs + zeroOffsetSecs;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -509,7 +518,7 @@ void setBootRelativeTimeForUnitTest(uint32_t secondsSinceBoot)
|
||||
{
|
||||
currentQuality = RTCQualityNone;
|
||||
zeroOffsetSecs = 0;
|
||||
timeStartMsec = millis() - (secondsSinceBoot * 1000);
|
||||
timeStartMs64 = Time::getMillisMonotonic() - ((uint64_t)secondsSinceBoot * 1000);
|
||||
lastSetFromPhoneNtpOrGps = 0;
|
||||
lastTimeValidationWarning = 0;
|
||||
}
|
||||
@@ -538,7 +547,7 @@ void setReadFromRTCUseSystemTimeForTests(bool enabled)
|
||||
void resetRTCStateForTests()
|
||||
{
|
||||
currentQuality = RTCQualityNone;
|
||||
timeStartMsec = 0;
|
||||
timeStartMs64 = 0;
|
||||
zeroOffsetSecs = 0;
|
||||
lastSetFromPhoneNtpOrGps = 0;
|
||||
lastTimeValidationWarning = 0;
|
||||
|
||||
@@ -99,7 +99,6 @@ bool EInkDisplay::forceDisplay(uint32_t msecLimit)
|
||||
// End the update process
|
||||
endUpdate();
|
||||
|
||||
LOG_DEBUG("done");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ bool EInkDynamicDisplay::determineMode()
|
||||
|
||||
resetRateLimiting(); // Once determineMode() ends, will have to wait again
|
||||
hashImage(); // Generate here, so we can still copy it to previousImageHash, even if we skip the comparison check
|
||||
LOG_DEBUG("determineMode(): "); // Begin log entry
|
||||
LOG_TRACE("determineMode(): "); // Begin log entry
|
||||
|
||||
// Once mode determined, any remaining checks will bypass
|
||||
checkCosmetic();
|
||||
@@ -232,9 +232,7 @@ void EInkDynamicDisplay::checkForPromotion()
|
||||
// Is it too soon for another frame of this type?
|
||||
void EInkDynamicDisplay::checkRateLimiting()
|
||||
{
|
||||
// Sanity check: millis() overflow - just let the update run..
|
||||
if (previousRunMs > millis())
|
||||
return;
|
||||
// No millis()-overflow guard needed: the Throttle checks below are wrap-correct already.
|
||||
|
||||
// Skip update: too soon for BACKGROUND
|
||||
if (frameFlags == BACKGROUND) {
|
||||
@@ -254,7 +252,7 @@ void EInkDynamicDisplay::checkRateLimiting()
|
||||
if (Throttle::isWithinTimespanMs(previousRunMs, 1000)) {
|
||||
refresh = SKIPPED;
|
||||
reason = EXCEEDED_RATELIMIT_FAST;
|
||||
LOG_DEBUG("refresh=SKIPPED, reason=EXCEEDED_RATELIMIT_FAST, frameFlags=0x%x", frameFlags);
|
||||
LOG_TRACE("refresh=SKIPPED, reason=EXCEEDED_RATELIMIT_FAST, frameFlags=0x%x", frameFlags);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -271,7 +269,7 @@ void EInkDynamicDisplay::checkCosmetic()
|
||||
if (frameFlags & COSMETIC) {
|
||||
refresh = FULL;
|
||||
reason = FLAGGED_COSMETIC;
|
||||
LOG_DEBUG("refresh=FULL, reason=FLAGGED_COSMETIC, frameFlags=0x%x", frameFlags);
|
||||
LOG_TRACE("refresh=FULL, reason=FLAGGED_COSMETIC, frameFlags=0x%x", frameFlags);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,7 +284,7 @@ void EInkDynamicDisplay::checkDemandingFast()
|
||||
if (frameFlags & DEMAND_FAST) {
|
||||
refresh = FAST;
|
||||
reason = FLAGGED_DEMAND_FAST;
|
||||
LOG_DEBUG("refresh=FAST, reason=FLAGGED_DEMAND_FAST, frameFlags=0x%x", frameFlags);
|
||||
LOG_TRACE("refresh=FAST, reason=FLAGGED_DEMAND_FAST, frameFlags=0x%x", frameFlags);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,7 +304,7 @@ void EInkDynamicDisplay::checkFrameMatchesPrevious()
|
||||
if (frameFlags == BACKGROUND && fastRefreshCount > 0) {
|
||||
refresh = FULL;
|
||||
reason = REDRAW_WITH_FULL;
|
||||
LOG_DEBUG("refresh=FULL, reason=REDRAW_WITH_FULL, frameFlags=0x%x", frameFlags);
|
||||
LOG_TRACE("refresh=FULL, reason=REDRAW_WITH_FULL, frameFlags=0x%x", frameFlags);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
@@ -314,7 +312,7 @@ void EInkDynamicDisplay::checkFrameMatchesPrevious()
|
||||
// Not redrawn, not COSMETIC, not DEMAND_FAST
|
||||
refresh = SKIPPED;
|
||||
reason = FRAME_MATCHED_PREVIOUS;
|
||||
LOG_DEBUG("refresh=SKIPPED, reason=FRAME_MATCHED_PREVIOUS, frameFlags=0x%x", frameFlags);
|
||||
LOG_TRACE("refresh=SKIPPED, reason=FRAME_MATCHED_PREVIOUS, frameFlags=0x%x", frameFlags);
|
||||
}
|
||||
|
||||
// Have too many fast-refreshes occurred consecutively, since last full refresh?
|
||||
@@ -328,7 +326,7 @@ void EInkDynamicDisplay::checkConsecutiveFastRefreshes()
|
||||
if (frameFlags & UNLIMITED_FAST) {
|
||||
refresh = FAST;
|
||||
reason = NO_OBJECTIONS;
|
||||
LOG_DEBUG("refresh=FAST, reason=UNLIMITED_FAST_MODE_ACTIVE, frameFlags=0x%x", frameFlags);
|
||||
LOG_TRACE("refresh=FAST, reason=UNLIMITED_FAST_MODE_ACTIVE, frameFlags=0x%x", frameFlags);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -336,7 +334,7 @@ void EInkDynamicDisplay::checkConsecutiveFastRefreshes()
|
||||
if (fastRefreshCount >= EINK_LIMIT_FASTREFRESH) {
|
||||
refresh = FULL;
|
||||
reason = EXCEEDED_LIMIT_FASTREFRESH;
|
||||
LOG_DEBUG("refresh=FULL, reason=EXCEEDED_LIMIT_FASTREFRESH, frameFlags=0x%x", frameFlags);
|
||||
LOG_TRACE("refresh=FULL, reason=EXCEEDED_LIMIT_FASTREFRESH, frameFlags=0x%x", frameFlags);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,13 +349,13 @@ void EInkDynamicDisplay::checkFastRequested()
|
||||
// If we want BACKGROUND to use fast. (FULL only when a limit is hit)
|
||||
refresh = FAST;
|
||||
reason = BACKGROUND_USES_FAST;
|
||||
LOG_DEBUG("refresh=FAST, reason=BACKGROUND_USES_FAST, fastRefreshCount=%lu, frameFlags=0x%x", fastRefreshCount,
|
||||
LOG_TRACE("refresh=FAST, reason=BACKGROUND_USES_FAST, fastRefreshCount=%lu, frameFlags=0x%x", fastRefreshCount,
|
||||
frameFlags);
|
||||
#else
|
||||
// If we do want to use FULL for BACKGROUND updates
|
||||
refresh = FULL;
|
||||
reason = FLAGGED_BACKGROUND;
|
||||
LOG_DEBUG("refresh=FULL, reason=FLAGGED_BACKGROUND");
|
||||
LOG_TRACE("refresh=FULL, reason=FLAGGED_BACKGROUND");
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -365,7 +363,7 @@ void EInkDynamicDisplay::checkFastRequested()
|
||||
if (frameFlags & RESPONSIVE) {
|
||||
refresh = FAST;
|
||||
reason = NO_OBJECTIONS;
|
||||
LOG_DEBUG("refresh=FAST, reason=NO_OBJECTIONS, fastRefreshCount=%lu, frameFlags=0x%x", fastRefreshCount, frameFlags);
|
||||
LOG_TRACE("refresh=FAST, reason=NO_OBJECTIONS, fastRefreshCount=%lu, frameFlags=0x%x", fastRefreshCount, frameFlags);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,7 +428,7 @@ void EInkDynamicDisplay::countGhostPixels()
|
||||
}
|
||||
}
|
||||
|
||||
LOG_DEBUG("ghostPixels=%hu, ", ghostPixelCount);
|
||||
LOG_TRACE("ghostPixels=%hu, ", ghostPixelCount);
|
||||
}
|
||||
|
||||
// Check if ghost pixel count exceeds the defined limit
|
||||
@@ -446,7 +444,7 @@ void EInkDynamicDisplay::checkExcessiveGhosting()
|
||||
if (ghostPixelCount > EINK_LIMIT_GHOSTING_PX) {
|
||||
refresh = FULL;
|
||||
reason = EXCEEDED_GHOSTINGLIMIT;
|
||||
LOG_DEBUG("refresh=FULL, reason=EXCEEDED_GHOSTINGLIMIT, frameFlags=0x%x", frameFlags);
|
||||
LOG_TRACE("refresh=FULL, reason=EXCEEDED_GHOSTINGLIMIT, frameFlags=0x%x", frameFlags);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -430,7 +430,7 @@ void Screen::showAlphanumericPicker(const char *message, const char *initialText
|
||||
void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||
std::function<void(const std::string &)> textCallback)
|
||||
{
|
||||
LOG_INFO("showTextInput called with header='%s', durationMs=%d", header ? header : "NULL", durationMs);
|
||||
LOG_INFO("showTextInput header='%s', durationMs=%d", header ? header : "NULL", durationMs);
|
||||
|
||||
// Start OnScreenKeyboardModule session (non-touch variant)
|
||||
OnScreenKeyboardModule::instance().start(header, initialText, durationMs, textCallback);
|
||||
@@ -581,7 +581,7 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
|
||||
#elif defined(USE_SPISSD1306)
|
||||
dispdev = new SSD1306Spi(SSD1306_RESET, SSD1306_RS, SSD1306_NSS, GEOMETRY_64_48);
|
||||
if (!dispdev->init()) {
|
||||
LOG_DEBUG("Error: SSD1306 not detected!");
|
||||
LOG_DEBUG("SSD1306 not detected");
|
||||
} else {
|
||||
static_cast<SSD1306Spi *>(dispdev)->setHorizontalOffset(32);
|
||||
LOG_INFO("SSD1306 init success");
|
||||
@@ -592,14 +592,14 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
|
||||
// runtime via config.yaml Display: Panel: HUB75.
|
||||
if (portduino_config.displayPanel == hub75) {
|
||||
#if defined(HAS_HUB75_NATIVE)
|
||||
LOG_DEBUG("Make HUB75Native!");
|
||||
LOG_DEBUG("Make HUB75Native");
|
||||
dispdev = new HUB75Native(address.address, -1, -1, GEOMETRY_RAWMODE, HW_I2C::I2C_ONE);
|
||||
#else
|
||||
LOG_ERROR("HUB75 panel requested but rpi-rgb-led-matrix not compiled in!");
|
||||
LOG_ERROR("HUB75 panel requested but rpi-rgb-led-matrix not compiled in");
|
||||
#endif
|
||||
} else if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
if (portduino_config.displayPanel != no_screen) {
|
||||
LOG_DEBUG("Make TFTDisplay!");
|
||||
LOG_DEBUG("Make TFTDisplay");
|
||||
dispdev = new TFTDisplay(address.address, -1, -1, geometry,
|
||||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||||
} else {
|
||||
@@ -610,7 +610,7 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
|
||||
}
|
||||
}
|
||||
#elif USE_TFTDISPLAY
|
||||
LOG_DEBUG("Make TFTDisplay!");
|
||||
LOG_DEBUG("Make TFTDisplay");
|
||||
dispdev = new TFTDisplay(address.address, -1, -1, geometry,
|
||||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||||
#elif defined(USE_EINK) && defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS) && !defined(MESHTASTIC_INCLUDE_INKHUD)
|
||||
@@ -1112,7 +1112,7 @@ int32_t Screen::runOnce()
|
||||
// Show boot screen for first logo_timeout seconds, then switch to normal operation.
|
||||
// serialSinceMsec adjusts for additional serial wait time during nRF52 bootup
|
||||
static bool showingBootScreen = true;
|
||||
if (showingBootScreen && (millis() > (logo_timeout + serialSinceMsec))) {
|
||||
if (showingBootScreen && Throttle::hasElapsed(serialSinceMsec, logo_timeout)) {
|
||||
LOG_INFO("Done with boot screen");
|
||||
stopBootScreen();
|
||||
showingBootScreen = false;
|
||||
@@ -1120,7 +1120,7 @@ int32_t Screen::runOnce()
|
||||
|
||||
#ifdef USERPREFS_OEM_TEXT
|
||||
static bool showingOEMBootScreen = true;
|
||||
if (showingOEMBootScreen && (millis() > ((logo_timeout / 2) + serialSinceMsec))) {
|
||||
if (showingOEMBootScreen && Throttle::hasElapsed(serialSinceMsec, logo_timeout / 2)) {
|
||||
LOG_INFO("Switch to OEM screen...");
|
||||
// Change frames.
|
||||
static FrameCallback bootOEMFrames[] = {graphics::UIRenderer::drawOEMBootScreen};
|
||||
@@ -1271,7 +1271,7 @@ int32_t Screen::runOnce()
|
||||
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC);
|
||||
#endif
|
||||
|
||||
LOG_DEBUG("LastScreenTransition exceeded %ums transition to next frame", (millis() - lastScreenTransition));
|
||||
LOG_DEBUG("LastScreenTransition exceeded %ums, next frame", (millis() - lastScreenTransition));
|
||||
handleOnPress();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -851,7 +851,7 @@ class LGFX : public lgfx::LGFX_Device
|
||||
#endif
|
||||
else {
|
||||
_panel_instance = new lgfx::Panel_NULL;
|
||||
LOG_ERROR("Unknown display panel configured!");
|
||||
LOG_ERROR("Unknown display panel configured");
|
||||
}
|
||||
|
||||
auto buscfg = _bus_instance.config();
|
||||
@@ -1187,7 +1187,7 @@ static inline uint16_t getThemeDefaultOffColor()
|
||||
|
||||
TFTDisplay::TFTDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus)
|
||||
{
|
||||
LOG_DEBUG("TFTDisplay!");
|
||||
LOG_DEBUG("TFTDisplay");
|
||||
|
||||
#ifdef TFT_BL
|
||||
GpioPin *p = new GpioHwPin(TFT_BL);
|
||||
@@ -1441,7 +1441,7 @@ void TFTDisplay::sdlLoop()
|
||||
if (portduino_config.displayPanel == x11) {
|
||||
lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)tft->_panel_instance;
|
||||
if (sdl_panel_->loop() && !shuttingDown) {
|
||||
LOG_WARN("Window Closed!");
|
||||
LOG_WARN("Window Closed");
|
||||
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||
inputBroker->injectInputEvent(&event);
|
||||
}
|
||||
@@ -1625,9 +1625,9 @@ bool TFTDisplay::connect()
|
||||
#ifdef HACKADAY_COMMUNICATOR
|
||||
bool beginStatus = tft->begin();
|
||||
if (beginStatus)
|
||||
LOG_DEBUG("TFT Success!");
|
||||
LOG_DEBUG("TFT Success");
|
||||
else
|
||||
LOG_ERROR("TFT Fail!");
|
||||
LOG_ERROR("TFT Fail");
|
||||
#else
|
||||
tft->init();
|
||||
#endif
|
||||
@@ -1656,7 +1656,7 @@ bool TFTDisplay::connect()
|
||||
this->linePixelBuffer = (uint16_t *)malloc(sizeof(uint16_t) * displayWidth);
|
||||
|
||||
if (!this->linePixelBuffer) {
|
||||
LOG_ERROR("Not enough memory to create TFT line buffer\n");
|
||||
LOG_ERROR("Not enough memory to create TFT line buffer");
|
||||
return false;
|
||||
}
|
||||
memaudit::add("display", sizeof(uint16_t) * displayWidth);
|
||||
@@ -1665,7 +1665,7 @@ bool TFTDisplay::connect()
|
||||
this->repaintChunkBuffer = (uint16_t *)malloc(sizeof(uint16_t) * displayWidth * kFullRepaintChunkRows);
|
||||
|
||||
if (!this->repaintChunkBuffer) {
|
||||
LOG_ERROR("Not enough memory to create TFT repaint chunk buffer\n");
|
||||
LOG_ERROR("Not enough memory to create TFT repaint chunk buffer");
|
||||
return false;
|
||||
}
|
||||
memaudit::add("display", sizeof(uint16_t) * displayWidth * kFullRepaintChunkRows);
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
#include "input/UpDownInterruptImpl1.h"
|
||||
#include "main.h"
|
||||
#include "mesh/Default.h"
|
||||
#if HAS_LORA_FEM
|
||||
#include "mesh/LoRaFEMInterface.h"
|
||||
#endif
|
||||
#include "mesh/MeshTypes.h"
|
||||
#include "mesh/RadioLibInterface.h"
|
||||
#include "modules/AdminModule.h"
|
||||
@@ -139,12 +142,34 @@ uint8_t test_count = 0;
|
||||
|
||||
void menuHandler::loraMenu()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "Frequency Slot", "LoRa Region"};
|
||||
enum optionsNumbers { Back = 0, DeviceRolePicker = 1, RadioPresetPicker = 2, FrequencySlot = 3, LoraPicker = 4 };
|
||||
static const char *optionsArray[] = {
|
||||
"Back",
|
||||
"Device Role",
|
||||
"Radio Preset",
|
||||
"Frequency Slot",
|
||||
"LoRa Region",
|
||||
#if HAS_LORA_FEM
|
||||
"FEM LNA",
|
||||
#endif
|
||||
};
|
||||
enum optionsNumbers {
|
||||
Back = 0,
|
||||
DeviceRolePicker = 1,
|
||||
RadioPresetPicker = 2,
|
||||
FrequencySlot = 3,
|
||||
LoraPicker = 4,
|
||||
#if HAS_LORA_FEM
|
||||
LoraFemLna = 5
|
||||
#endif
|
||||
};
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "LoRa Actions";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
#if HAS_LORA_FEM
|
||||
bannerOptions.optionsCount = loraFEMInterface.isLnaCanControl() ? 6 : 5;
|
||||
#else
|
||||
bannerOptions.optionsCount = 5;
|
||||
#endif
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Back) {
|
||||
// No action
|
||||
@@ -157,6 +182,11 @@ void menuHandler::loraMenu()
|
||||
} else if (selected == LoraPicker) {
|
||||
menuHandler::menuQueue = menuHandler::LoraPicker;
|
||||
}
|
||||
#if HAS_LORA_FEM
|
||||
else if (selected == LoraFemLna) {
|
||||
menuHandler::menuQueue = menuHandler::LoraFemLnaToggleMenu;
|
||||
}
|
||||
#endif
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
@@ -193,7 +223,7 @@ static void applyLoraRegion(meshtastic_Config_LoRaConfig_RegionCode region, bool
|
||||
// flip the region right back. The user picked the region, so the preset follows it.
|
||||
const RegionInfo *newRegion = getRegion(region);
|
||||
if (config.lora.use_preset && !newRegion->supportsPreset(config.lora.modem_preset)) {
|
||||
LOG_INFO("Preset %s not available in %s, using default %s",
|
||||
LOG_INFO("Preset %s unavailable in %s, use default %s",
|
||||
DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, true), newRegion->name,
|
||||
DisplayFormatters::getModemPresetDisplayName(newRegion->getDefaultPreset(), false, true));
|
||||
config.lora.modem_preset = newRegion->getDefaultPreset();
|
||||
@@ -319,7 +349,7 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||
menuQueue = HamModeConfirm;
|
||||
screen->runNow();
|
||||
} else if (owner.is_licensed) {
|
||||
LOG_INFO("Licensed user chose a non-ham region; prompting to revert licensed mode");
|
||||
LOG_INFO("Licensed user chose non-ham region; prompt to revert licensed mode");
|
||||
pendingRegion = selectedRegion;
|
||||
menuQueue = LicensedToNormalConfirm;
|
||||
screen->runNow();
|
||||
@@ -439,10 +469,10 @@ void menuHandler::FrequencySlotPicker()
|
||||
if (denominator > 0.0) {
|
||||
numChannels = static_cast<uint32_t>(round(numerator / denominator));
|
||||
} else {
|
||||
LOG_WARN("Invalid region configuration: non-positive channel spacing/width");
|
||||
LOG_WARN("Invalid region config: non-positive channel spacing/width");
|
||||
}
|
||||
} else {
|
||||
LOG_WARN("Region not set, cannot calculate number of channels");
|
||||
LOG_WARN("Region not set, can't calc channel count");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -944,7 +974,7 @@ void menuHandler::deleteMessagesMenu()
|
||||
|
||||
// This only appears in non-ALL modes
|
||||
if (selected == DeleteThis) {
|
||||
LOG_INFO("Deleting all messages in this thread");
|
||||
LOG_INFO("Deleting all messages in thread");
|
||||
|
||||
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
|
||||
messageStore.deleteAllMessagesInChannel(ch);
|
||||
@@ -1792,7 +1822,7 @@ void menuHandler::resetNodeDBMenu()
|
||||
disableBluetooth();
|
||||
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||||
} else if (selected == 2) {
|
||||
LOG_INFO("Initiate node-db reset but keeping favorites");
|
||||
LOG_INFO("Initiate node-db reset, keep favorites");
|
||||
nodeDB->resetNodes(1);
|
||||
disableBluetooth();
|
||||
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||||
@@ -2359,7 +2389,7 @@ void menuHandler::removeFavoriteMenu()
|
||||
void menuHandler::traceRouteMenu()
|
||||
{
|
||||
screen->showNodePicker("Node to Trace", 30000, [](uint32_t nodenum) -> void {
|
||||
LOG_INFO("Menu: Node picker selected node 0x%08x, traceRouteModule=%p", nodenum, traceRouteModule);
|
||||
LOG_INFO("Menu: Node picker selected 0x%08x, traceRouteModule=%p", nodenum, traceRouteModule);
|
||||
if (traceRouteModule) {
|
||||
traceRouteModule->startTraceRoute(nodenum);
|
||||
}
|
||||
@@ -2804,6 +2834,49 @@ void menuHandler::messageBubblesMenu()
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
#if HAS_LORA_FEM
|
||||
void menuHandler::LoRaFEMLNAToggleMenu()
|
||||
{
|
||||
static const LoRaFEMLNAToggleOption femToggleOptions[] = {
|
||||
{"Back", OptionsAction::Back},
|
||||
{"Enabled", OptionsAction::Select, meshtastic_Config_LoRaConfig_FEM_LNA_Mode_ENABLED},
|
||||
{"Disabled", OptionsAction::Select, meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED},
|
||||
};
|
||||
constexpr size_t toggleCount = sizeof(femToggleOptions) / sizeof(femToggleOptions[0]);
|
||||
static std::array<const char *, toggleCount> toggleLabels{};
|
||||
|
||||
auto bannerOptions = createStaticBannerOptions(
|
||||
"FEM LNA", femToggleOptions, toggleLabels, [](const LoRaFEMLNAToggleOption &option, int) -> void {
|
||||
if (option.action == OptionsAction::Back) {
|
||||
menuQueue = LoraMenu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!option.hasValue || config.lora.fem_lna_mode == option.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool enabled = option.value != meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED;
|
||||
config.lora.fem_lna_mode = option.value;
|
||||
loraFEMInterface.setLNAEnable(enabled);
|
||||
service->reloadConfig(SEGMENT_CONFIG);
|
||||
LOG_INFO("FEM LNA %s", enabled ? "enabled" : "disabled");
|
||||
});
|
||||
|
||||
int initialSelection = 0;
|
||||
for (size_t i = 0; i < toggleCount; ++i) {
|
||||
if (femToggleOptions[i].hasValue && config.lora.fem_lna_mode == femToggleOptions[i].value) {
|
||||
initialSelection = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
bannerOptions.InitialSelected = initialSelection;
|
||||
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
#endif
|
||||
|
||||
void menuHandler::themeMenu()
|
||||
{
|
||||
// Build menu dynamically from the theme table.
|
||||
@@ -3013,6 +3086,11 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
||||
case LicensedToNormalConfirm:
|
||||
licensedToNormalConfirmMenu();
|
||||
break;
|
||||
#if HAS_LORA_FEM
|
||||
case LoraFemLnaToggleMenu:
|
||||
LoRaFEMLNAToggleMenu();
|
||||
break;
|
||||
#endif
|
||||
}
|
||||
menuQueue = MenuNone;
|
||||
}
|
||||
|
||||
@@ -59,7 +59,10 @@ class menuHandler
|
||||
MessageBubblesMenu,
|
||||
ThemeMenu,
|
||||
HamModeConfirm,
|
||||
LicensedToNormalConfirm
|
||||
LicensedToNormalConfirm,
|
||||
#if HAS_LORA_FEM
|
||||
LoraFemLnaToggleMenu
|
||||
#endif
|
||||
};
|
||||
static screenMenus menuQueue;
|
||||
static uint32_t pickedNodeNum; // node selected by NodePicker for ManageNodeMenu
|
||||
@@ -120,6 +123,9 @@ class menuHandler
|
||||
static void textMessageMenu();
|
||||
static void hamModeConfirmMenu();
|
||||
static void licensedToNormalConfirmMenu();
|
||||
#if HAS_LORA_FEM
|
||||
static void LoRaFEMLNAToggleMenu();
|
||||
#endif
|
||||
|
||||
// Lifted out of its banner-callback lambda so it is reachable without a Screen. The lambda only
|
||||
// ever runs via screen->showOverlayBanner(), which is why nothing here was unit-testable.
|
||||
@@ -159,6 +165,9 @@ using NodeNameOption = MenuOption<bool>;
|
||||
using PositionMenuOption = MenuOption<int>;
|
||||
using ManageNodeOption = MenuOption<int>;
|
||||
using ClockFaceOption = MenuOption<bool>;
|
||||
#if HAS_LORA_FEM
|
||||
using LoRaFEMLNAToggleOption = MenuOption<meshtastic_Config_LoRaConfig_FEM_LNA_Mode>;
|
||||
#endif
|
||||
|
||||
} // namespace graphics
|
||||
#endif
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "graphics/TFTColorRegions.h"
|
||||
#include "graphics/TFTPalette.h"
|
||||
#include "graphics/TimeFormatters.h"
|
||||
#include "graphics/draw/NotificationRenderer.h"
|
||||
#include "graphics/emotes.h"
|
||||
#include "main.h"
|
||||
#include "meshUtils.h"
|
||||
@@ -1128,6 +1129,9 @@ void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const mesht
|
||||
if (packet.from != 0) {
|
||||
hasUnreadMessage = true;
|
||||
const bool suppressBanner = cannedMessageModule && cannedMessageModule->isFreeTextActive();
|
||||
// Don't let the pop-up clobber a menu/picker the user is interacting with; the wake below
|
||||
// still happens so a message can light the screen back up.
|
||||
const bool menuShowing = NotificationRenderer::isMenuShowing();
|
||||
|
||||
// Determine if message belongs to a muted channel
|
||||
bool isChannelMuted = false;
|
||||
@@ -1222,7 +1226,7 @@ void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const mesht
|
||||
screen->setOn(true);
|
||||
}
|
||||
|
||||
if (!suppressBanner) {
|
||||
if (!suppressBanner && !menuShowing) {
|
||||
screen->showSimpleBanner(banner, inThread ? 1000 : 3000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "graphics/images.h"
|
||||
#include "input/RotaryEncoderInterruptImpl1.h"
|
||||
#include "input/UpDownInterruptImpl1.h"
|
||||
#include "mesh/Throttle.h"
|
||||
#if HAS_BUTTON
|
||||
#include "input/ButtonThread.h"
|
||||
#endif
|
||||
@@ -84,7 +85,7 @@ static inline graphics::NotificationRenderer::BannerFont parseFontTagPrefix(cons
|
||||
{
|
||||
// Tags must be at the start of the line:
|
||||
// [S] small, [M] medium, [L] large
|
||||
if (p && p[0] == '[' && p[2] == ']' && p[1] != '\0') {
|
||||
if (p && p[0] == '[' && p[1] != '\0' && p[2] == ']') {
|
||||
char t = p[1];
|
||||
if (t == 'S') {
|
||||
p += 3;
|
||||
@@ -136,6 +137,26 @@ static inline uint8_t effectiveLineHeightForBannerLine(graphics::NotificationRen
|
||||
return (height > 3) ? (height - 3) : height;
|
||||
}
|
||||
|
||||
const char *graphics::NotificationRenderer::resolveBannerLine(uint16_t lineIndex, const char *rawLine, BannerFont &lineFont)
|
||||
{
|
||||
lineFont = BANNER_FONT_DEFAULT;
|
||||
bool tagAware = (current_notification_type == notificationTypeEnum::text_banner ||
|
||||
current_notification_type == notificationTypeEnum::pairing_pin) &&
|
||||
alertBannerOptions == 0;
|
||||
if (!tagAware)
|
||||
return rawLine;
|
||||
if (lineIndex < alertBannerLineCount) {
|
||||
lineFont = alertBannerLineFonts[lineIndex];
|
||||
return alertBannerLines[lineIndex];
|
||||
}
|
||||
// The parsed-line cache doesn't cover this line (the banner text was stored without a
|
||||
// re-parse, or a draw raced the parse from another task): strip the tag here too, so it
|
||||
// acts as a font change and never renders as literal text - the BLE pair PIN banner
|
||||
// prefixes its PIN line with [M].
|
||||
lineFont = parseFontTagPrefix(rawLine);
|
||||
return rawLine;
|
||||
}
|
||||
|
||||
void graphics::NotificationRenderer::parseBannerMessageWithFonts(const char *message)
|
||||
{
|
||||
alertBannerLineCount = 0;
|
||||
@@ -233,7 +254,7 @@ void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayU
|
||||
// Handle text_input notifications first - they have their own timeout/banner logic
|
||||
if (current_notification_type == notificationTypeEnum::text_input) {
|
||||
// Check for timeout and reset if needed for text input
|
||||
if (millis() > alertBannerUntil && alertBannerUntil > 0) {
|
||||
if (alertBannerUntil > 0 && Throttle::deadlinePassed(alertBannerUntil)) {
|
||||
resetBanner();
|
||||
return;
|
||||
}
|
||||
@@ -241,7 +262,8 @@ void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayU
|
||||
return;
|
||||
}
|
||||
|
||||
if (millis() > alertBannerUntil && alertBannerUntil > 0) {
|
||||
// 0 means "no deadline set", and reads as long expired - test it first.
|
||||
if (alertBannerUntil > 0 && Throttle::deadlinePassed(alertBannerUntil)) {
|
||||
resetBanner();
|
||||
}
|
||||
|
||||
@@ -845,9 +867,6 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
BannerFont lineFonts[totalLines] = {};
|
||||
uint8_t lineEffectiveHeights[totalLines] = {0};
|
||||
const char *renderLines[totalLines] = {0};
|
||||
bool useTaggedBannerFonts = (current_notification_type == notificationTypeEnum::text_banner ||
|
||||
current_notification_type == notificationTypeEnum::pairing_pin) &&
|
||||
alertBannerOptions == 0;
|
||||
|
||||
if (maxWidth != 0)
|
||||
is_picker = true;
|
||||
@@ -860,12 +879,8 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
uint16_t widestLineWithBars = 0;
|
||||
|
||||
while (lines[lineCount] != nullptr) {
|
||||
const char *renderText = lines[lineCount];
|
||||
BannerFont lineFont = BANNER_FONT_DEFAULT;
|
||||
if (useTaggedBannerFonts && lineCount < alertBannerLineCount) {
|
||||
renderText = alertBannerLines[lineCount];
|
||||
lineFont = alertBannerLineFonts[lineCount];
|
||||
}
|
||||
const char *renderText = resolveBannerLine(lineCount, lines[lineCount], lineFont);
|
||||
renderLines[lineCount] = renderText;
|
||||
lineFonts[lineCount] = lineFont;
|
||||
lineEffectiveHeights[lineCount] = effectiveLineHeightForBannerLine(lineFont);
|
||||
@@ -879,10 +894,10 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
|
||||
if (current_notification_type == notificationTypeEnum::node_picker) {
|
||||
char measureBuffer[64] = {0};
|
||||
strncpy(measureBuffer, lines[lineCount], std::min<size_t>(lineLengths[lineCount], sizeof(measureBuffer) - 1));
|
||||
strncpy(measureBuffer, renderText, std::min<size_t>(lineLengths[lineCount], sizeof(measureBuffer) - 1));
|
||||
lineWidths[lineCount] = UIRenderer::measureStringWithEmotes(display, measureBuffer);
|
||||
} else {
|
||||
lineWidths[lineCount] = display->getStringWidth(lines[lineCount], lineLengths[lineCount], true);
|
||||
lineWidths[lineCount] = display->getStringWidth(renderText, lineLengths[lineCount], true);
|
||||
}
|
||||
|
||||
// Consider extra width for signal bars on lines that contain "Signal:"
|
||||
@@ -1213,7 +1228,16 @@ void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiStat
|
||||
|
||||
bool NotificationRenderer::isOverlayBannerShowing()
|
||||
{
|
||||
return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil);
|
||||
// Here 0 means "show indefinitely", so it must short-circuit the comparison.
|
||||
return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || !Throttle::deadlinePassed(alertBannerUntil));
|
||||
}
|
||||
|
||||
bool NotificationRenderer::isMenuShowing()
|
||||
{
|
||||
// A menu, picker, keyboard, or pairing-PIN overlay - anything interactive, as opposed to a plain
|
||||
// informational text banner (which has no options and type text_banner). Menus don't set a
|
||||
// notificationType of their own, so options are the only thing distinguishing them.
|
||||
return isOverlayBannerShowing() && (alertBannerOptions > 0 || current_notification_type != notificationTypeEnum::text_banner);
|
||||
}
|
||||
|
||||
} // namespace graphics
|
||||
|
||||
@@ -38,6 +38,10 @@ class NotificationRenderer
|
||||
static uint8_t alertBannerLineCount;
|
||||
static BannerFont alertBannerLineFonts[MAX_LINES + 1];
|
||||
static void parseBannerMessageWithFonts(const char *message);
|
||||
// Decide what text and font a banner line actually renders with: parsed (tag-stripped)
|
||||
// line if the cache covers it, otherwise the raw line with any leading font tag stripped
|
||||
// on the fly. Exposed for unit tests.
|
||||
static const char *resolveBannerLine(uint16_t lineIndex, const char *rawLine, BannerFont &lineFont);
|
||||
static void resetBanner();
|
||||
static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
@@ -53,6 +57,7 @@ class NotificationRenderer
|
||||
static void drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
static void drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
static bool isOverlayBannerShowing();
|
||||
static bool isMenuShowing();
|
||||
|
||||
static graphics::notificationTypeEnum current_notification_type;
|
||||
};
|
||||
|
||||
@@ -582,7 +582,7 @@ void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y,
|
||||
}
|
||||
} else if (!gps->getHasLock() && !config.position.fixed_position) {
|
||||
if (strcmp(mode, "line1") == 0) {
|
||||
strcpy(displayLine, "No GPS Lock");
|
||||
strcpy(displayLine, gps->getHasTime() ? "GPS Time Only" : "No GPS Lock");
|
||||
display->drawString(x, y, displayLine);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -54,7 +54,7 @@ int32_t EInk::runOnce()
|
||||
// - polling timeout
|
||||
// - other error (derived classes)
|
||||
if (failed) {
|
||||
LOG_WARN("Display update failed. Check wiring & power supply.");
|
||||
LOG_WARN("Display update failed. Check wiring & power supply");
|
||||
updateRunning = false;
|
||||
failed = false;
|
||||
return disable();
|
||||
|
||||
@@ -54,7 +54,7 @@ int32_t EInk::runOnce()
|
||||
// - polling timeout
|
||||
// - other error (derived classes)
|
||||
if (failed) {
|
||||
LOG_WARN("Display update failed. Check wiring & power supply.");
|
||||
LOG_WARN("Display update failed. Check wiring & power supply");
|
||||
updateRunning = false;
|
||||
failed = false;
|
||||
return disable();
|
||||
|
||||
@@ -24,4 +24,4 @@ build_flags =
|
||||
-D HAS_BUTTON=0 ; Suppress default ButtonThread
|
||||
lib_deps =
|
||||
# renovate: datasource=github-tags depName=GFX_Root packageName=ZinggJM/GFX_Root
|
||||
https://github.com/ZinggJM/GFX_Root/archive/3195764e352a0d2567c8d277ac408ca7293a99b0.zip ; Used by InkHUD as a "slimmer" version of AdafruitGFX
|
||||
https://github.com/ZinggJM/GFX_Root.git#3195764e352a0d2567c8d277ac408ca7293a99b0 ; Used by InkHUD as a "slimmer" version of AdafruitGFX
|
||||
@@ -96,7 +96,7 @@ template <typename T> class FlashData
|
||||
|
||||
f.close();
|
||||
} else {
|
||||
LOG_ERROR("Could not open / read %s", filename.c_str());
|
||||
LOG_ERROR("Can't open/read %s", filename.c_str());
|
||||
okay = false;
|
||||
}
|
||||
#else
|
||||
@@ -135,10 +135,10 @@ template <typename T> class FlashData
|
||||
bool writeSucceeded = f.close();
|
||||
|
||||
if (!writeSucceeded) {
|
||||
LOG_ERROR("Can't write data!");
|
||||
LOG_ERROR("Can't write data");
|
||||
}
|
||||
#else
|
||||
LOG_ERROR("ERROR: Filesystem not implemented\n");
|
||||
LOG_ERROR("Filesystem not implemented");
|
||||
#endif
|
||||
}
|
||||
};
|
||||
@@ -165,7 +165,7 @@ inline void clearFlashData()
|
||||
file = dir.openNextFile();
|
||||
}
|
||||
#else
|
||||
LOG_ERROR("ERROR: Filesystem not implemented\n");
|
||||
LOG_ERROR("Filesystem not implemented");
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -405,7 +405,7 @@ void tftSetup(void)
|
||||
PacketAPI::create(PacketServer::init());
|
||||
deviceScreen->init(new PacketClient);
|
||||
} else {
|
||||
LOG_INFO("Running without TFT display!");
|
||||
LOG_INFO("Running without TFT display");
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -226,7 +226,7 @@ int32_t ButtonThread::runOnce()
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "RotaryEncoderImpl.h"
|
||||
#include "InputBroker.h"
|
||||
#include "RotaryEncoder.h"
|
||||
#include "mesh/Throttle.h"
|
||||
#ifdef ARCH_ESP32
|
||||
#include "sleep.h"
|
||||
#endif
|
||||
@@ -66,7 +67,7 @@ void RotaryEncoderImpl::pollOnce()
|
||||
|
||||
static uint32_t lastPressed = millis();
|
||||
if (rotary->readButton() == RotaryEncoder::ButtonState::BUTTON_PRESSED) {
|
||||
if (lastPressed + 200 < millis()) {
|
||||
if (Throttle::hasElapsed(lastPressed, 200)) {
|
||||
LOG_DEBUG("Rotary event Press");
|
||||
lastPressed = millis();
|
||||
e.inputEvent = this->eventPressed;
|
||||
|
||||
+23
-17
@@ -16,6 +16,7 @@
|
||||
#include "RadioLibInterface.h"
|
||||
#include "ReliableRouter.h"
|
||||
#include "TransmitHistory.h"
|
||||
#include "UptimeClock.h"
|
||||
#include "airtime.h"
|
||||
#include "buzz.h"
|
||||
#include "power/PowerHAL.h"
|
||||
@@ -316,11 +317,13 @@ __attribute__((weak, noinline)) bool loopCanSleep()
|
||||
|
||||
// Weak empty variant initialization function.
|
||||
// May be redefined by variant files.
|
||||
void lateInitVariant() __attribute__((weak));
|
||||
void lateInitVariant() {}
|
||||
// noinline: weak default and call site share this TU, so LTO would inline the empty body and
|
||||
// never link the variant's strong override. nrf52_lto.py's _VARIANT_OVERRIDES guards this.
|
||||
__attribute__((noinline)) void lateInitVariant() __attribute__((weak));
|
||||
__attribute__((noinline)) void lateInitVariant() {}
|
||||
|
||||
void earlyInitVariant() __attribute__((weak));
|
||||
void earlyInitVariant() {}
|
||||
__attribute__((noinline)) void earlyInitVariant() __attribute__((weak));
|
||||
__attribute__((noinline)) void earlyInitVariant() {}
|
||||
|
||||
// NRF52 (and probably other platforms) can report when system is in power failure mode
|
||||
// (eg. too low battery voltage) and operating it is unsafe (data corruption, bootloops, etc).
|
||||
@@ -544,9 +547,9 @@ void setup()
|
||||
EncryptedStorage::initLocked();
|
||||
if (!EncryptedStorage::isUnlocked()) {
|
||||
if (!EncryptedStorage::isProvisioned()) {
|
||||
LOG_WARN("Lockdown: Device not provisioned - connect and set a passphrase to unlock storage");
|
||||
LOG_WARN("Lockdown: Device not provisioned - set passphrase to unlock storage");
|
||||
} else {
|
||||
LOG_WARN("Lockdown: Device locked - connect and provide passphrase to unlock storage");
|
||||
LOG_WARN("Lockdown: Device locked - provide passphrase to unlock storage");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -563,7 +566,7 @@ void setup()
|
||||
if (EncryptedStorage::isProvisioned()) {
|
||||
enableAPProtect();
|
||||
} else {
|
||||
LOG_INFO("APPROTECT deferred: device not yet provisioned");
|
||||
LOG_INFO("APPROTECT deferred: not provisioned");
|
||||
}
|
||||
#elif defined(MESHTASTIC_ENABLE_APPROTECT)
|
||||
// Lockdown without encrypted storage shouldn't be reachable per
|
||||
@@ -650,7 +653,7 @@ void setup()
|
||||
// the bus behind it is scanned right below and its devices are registered
|
||||
// once, so the link has to be up by then
|
||||
if (!sensecapIndicator->wait_ready(5000))
|
||||
LOG_ERROR("RP2040 co-processor did not answer, its sensors, GPS and SD card are unavailable this session");
|
||||
LOG_ERROR("RP2040 co-processor no reply; sensors, GPS, SD card unavailable this session");
|
||||
#endif
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_I2C
|
||||
@@ -693,7 +696,7 @@ void setup()
|
||||
#ifdef ARCH_ESP32
|
||||
// Don't init display if we don't have one or we are waking headless due to a timer event
|
||||
if (wakeCause == ESP_SLEEP_WAKEUP_TIMER) {
|
||||
LOG_DEBUG("suppress screen wake because this is a headless timer wakeup");
|
||||
LOG_DEBUG("suppress screen wake: headless timer wakeup");
|
||||
i2cScanner->setSuppressScreen();
|
||||
}
|
||||
#endif
|
||||
@@ -755,7 +758,7 @@ void setup()
|
||||
break;
|
||||
default:
|
||||
// use this as default since it's also just zero
|
||||
LOG_WARN("kb_info.type is unknown(0x%02x), setting kb_model=0x00", kb_info.type);
|
||||
LOG_WARN("kb_info.type unknown(0x%02x), set kb_model=0x00", kb_info.type);
|
||||
kb_model = 0x00;
|
||||
}
|
||||
}
|
||||
@@ -1358,12 +1361,15 @@ void loop()
|
||||
{
|
||||
runASAP = false;
|
||||
|
||||
// The single writer of the monotonic wrap carry; every other caller only reads it.
|
||||
Time::serviceMonotonic();
|
||||
|
||||
#if defined(MESHTASTIC_ENCRYPTED_STORAGE) && defined(MESHTASTIC_PHONEAPI_ACCESS_CONTROL)
|
||||
if (lockdownDisablePending) {
|
||||
lockdownDisablePending = false;
|
||||
LOG_INFO("Lockdown: disabling - reverting encrypted storage to plaintext");
|
||||
if (nodeDB->disableLockdownToPlaintext()) {
|
||||
LOG_INFO("Lockdown: disabled, rebooting into normal mode");
|
||||
LOG_INFO("Lockdown: disabled, reboot to normal mode");
|
||||
PhoneAPI::broadcastLockdownStatus(meshtastic_LockdownStatus_State_DISABLED, "", 0, 0, 0);
|
||||
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
|
||||
} else {
|
||||
@@ -1371,14 +1377,14 @@ void loop()
|
||||
// The DEK file is still present (it's deleted last), so the device
|
||||
// stays in lockdown and the operator can retry disable. Surface
|
||||
// the failure rather than leaving the client hanging.
|
||||
LOG_ERROR("Lockdown: disable revert failed - device remains in lockdown");
|
||||
LOG_ERROR("Lockdown: disable revert failed - still in lockdown");
|
||||
PhoneAPI::broadcastLockdownStatus(meshtastic_LockdownStatus_State_LOCKED, "disable_failed", 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (lockdownReloadPending) {
|
||||
lockdownReloadPending = false;
|
||||
LOG_INFO("Lockdown: reloading config from disk after unlock");
|
||||
LOG_INFO("Lockdown: reload config after unlock");
|
||||
bool reloadOk = nodeDB->reloadFromDisk();
|
||||
if (!reloadOk) {
|
||||
// Storage decrypt/decode failed during reload. Treat as
|
||||
@@ -1389,7 +1395,7 @@ void loop()
|
||||
// might have), and notify clients. Storage will be locked
|
||||
// on next boot anyway; deferring to the user-visible
|
||||
// notification path is sufficient for now.
|
||||
LOG_ERROR("Lockdown: reload failed - locking and notifying clients");
|
||||
LOG_ERROR("Lockdown: reload failed - lock and notify clients");
|
||||
EncryptedStorage::lockNow();
|
||||
PhoneAPI::revokeAllAuth();
|
||||
}
|
||||
@@ -1415,14 +1421,14 @@ void loop()
|
||||
// sessions to grant. Hard lock (token deleted, DEK
|
||||
// zeroed) and reboot. Operator must re-enter passphrase.
|
||||
if (EncryptedStorage::getBootsRemaining() == 0) {
|
||||
LOG_WARN("Lockdown: session limit reached and boot budget exhausted, locking and rebooting");
|
||||
LOG_WARN("Lockdown: session limit hit, boot budget exhausted - lock and reboot");
|
||||
EncryptedStorage::lockNow();
|
||||
PhoneAPI::revokeAllAuth();
|
||||
PhoneAPI::broadcastLockdownStatus(meshtastic_LockdownStatus_State_LOCKED, "session_budget_exhausted", 0, 0, 0);
|
||||
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
|
||||
} else {
|
||||
uint8_t newBoots = EncryptedStorage::consumeSessionBoot();
|
||||
LOG_WARN("Lockdown: session expired, rolled to next budget slot (boots=%u remaining)", newBoots);
|
||||
LOG_WARN("Lockdown: session expired, next budget slot (boots=%u left)", newBoots);
|
||||
PhoneAPI::revokeAllAuth();
|
||||
meshtastic_security::lockScreen();
|
||||
// Signal clients that they need to re-auth on this
|
||||
@@ -1484,7 +1490,7 @@ void loop()
|
||||
ch341Hal->checkError();
|
||||
}
|
||||
if (portduino_status.LoRa_in_error && rebootAtMsec == 0) {
|
||||
LOG_ERROR("LoRa in error detected, attempting to recover");
|
||||
LOG_ERROR("LoRa error detected, recovering");
|
||||
router->addInterface(nullptr);
|
||||
if (portduino_config.lora_spi_dev == "ch341") {
|
||||
if (ch341Hal != nullptr) {
|
||||
|
||||
@@ -497,6 +497,21 @@ bool Channels::isWellKnownChannel(ChannelIndex chIndex)
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Channels::isEventChannel(ChannelIndex chIndex)
|
||||
{
|
||||
#if USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL && defined(USERPREFS_CHANNEL_0_PSK)
|
||||
static const uint8_t configuredEventPsk[] = USERPREFS_CHANNEL_0_PSK;
|
||||
static_assert(sizeof(configuredEventPsk) == 16 || sizeof(configuredEventPsk) == 32,
|
||||
"USERPREFS_CHANNEL_0_PSK must be an AES-128 or AES-256 key");
|
||||
CryptoKey effectiveKey = getKey(chIndex);
|
||||
return effectiveKey.length == sizeof(configuredEventPsk) &&
|
||||
memcmp(effectiveKey.bytes, configuredEventPsk, sizeof(configuredEventPsk)) == 0;
|
||||
#else
|
||||
(void)chIndex;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Channels::hasDefaultChannel()
|
||||
{
|
||||
// If we don't use a preset or the default frequency slot, or we override the frequency, we don't have a default channel
|
||||
|
||||
+8
-1
@@ -5,6 +5,10 @@
|
||||
#include "mesh-pb-constants.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
#if USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL && !defined(USERPREFS_CHANNEL_0_PSK)
|
||||
#error "USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL requires USERPREFS_CHANNEL_0_PSK"
|
||||
#endif
|
||||
|
||||
/** A channel number (index into the channel table)
|
||||
*/
|
||||
typedef uint8_t ChannelIndex;
|
||||
@@ -95,6 +99,9 @@ class Channels
|
||||
// matches the current preset's name and PSK byte 1.
|
||||
bool isWellKnownChannel(ChannelIndex chIndex);
|
||||
|
||||
// Returns true if this channel's effective key matches USERPREFS_CHANNEL_0_PSK.
|
||||
bool isEventChannel(ChannelIndex chIndex);
|
||||
|
||||
// Returns true if we can be reached via a channel with the default settings given a region and modem preset
|
||||
bool hasDefaultChannel();
|
||||
|
||||
@@ -164,4 +171,4 @@ bool channelFileUsesPublicKey(const meshtastic_ChannelFile &cf, ChannelIndex chI
|
||||
|
||||
static const uint8_t eventpsk[] = {0x38, 0x4b, 0xbc, 0xc0, 0x1d, 0xc0, 0x22, 0xd1, 0x81, 0xbf, 0x36,
|
||||
0xb8, 0x61, 0x21, 0xe1, 0xfb, 0x96, 0xb7, 0x2e, 0x55, 0xbf, 0x74,
|
||||
0x22, 0x7e, 0x9d, 0x6a, 0xfb, 0x48, 0xd6, 0x4c, 0xb1, 0xa1};
|
||||
0x22, 0x7e, 0x9d, 0x6a, 0xfb, 0x48, 0xd6, 0x4c, 0xb1, 0xa1};
|
||||
@@ -336,7 +336,7 @@ bool CryptoEngine::setDHPublicKey(uint8_t *pubKey)
|
||||
// Calculate the shared secret with the specified node's public key and our private key
|
||||
// This includes an internal weak key check, which among other things looks for an all 0 public key and shared key.
|
||||
if (!Curve25519::dh2(shared_key, local_priv)) {
|
||||
LOG_WARN("Curve25519DH step 2 failed!");
|
||||
LOG_WARN("Curve25519DH step 2 failed");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -373,7 +373,7 @@ concurrency::Lock *cryptLock;
|
||||
|
||||
void CryptoEngine::setKey(const CryptoKey &k)
|
||||
{
|
||||
LOG_DEBUG("Use AES%d key!", k.length * 8);
|
||||
LOG_DEBUG("Use AES%d key", k.length * 8);
|
||||
key = k;
|
||||
}
|
||||
|
||||
@@ -389,7 +389,7 @@ void CryptoEngine::encryptPacket(uint32_t fromNode, uint64_t packetId, size_t nu
|
||||
if (numBytes <= MAX_BLOCKSIZE) {
|
||||
encryptAESCtr(key, nonce, numBytes, bytes);
|
||||
} else {
|
||||
LOG_ERROR("Packet too large for crypto engine: %d. noop encryption!", numBytes);
|
||||
LOG_ERROR("Packet too large for crypto engine: %d. noop encryption", numBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,7 +486,7 @@ bool SensecapIndicator::handle_packet(size_t payload_len)
|
||||
LOG_WARN("Request 0x%08x nacked by the co-processor", expected_id);
|
||||
request_nacked = true;
|
||||
} else if (message.id == 0) {
|
||||
LOG_WARN("Co-processor could not decode a frame");
|
||||
LOG_WARN("Co-processor can't decode a frame");
|
||||
}
|
||||
return true;
|
||||
case meshtastic_InterdeviceMessage_sd_info_tag:
|
||||
|
||||
@@ -104,11 +104,11 @@ template <typename T> bool LR11x0Interface<T>::init()
|
||||
|
||||
// DIO3 is free to be used as an IRQ only while no TCXO Vref is driven on it
|
||||
if (tcxoVoltage > 0)
|
||||
LOG_DEBUG("LR11x0 TCXO Vref %f V on DIO3 (DIO3 unavailable as an IRQ)", tcxoVoltage);
|
||||
LOG_DEBUG("LR11x0 TCXO Vref %f V on DIO3 (DIO3 unavailable as IRQ)", tcxoVoltage);
|
||||
else
|
||||
LOG_DEBUG("LR11x0 no TCXO Vref, XTAL only (DIO3 free as an IRQ)");
|
||||
LOG_DEBUG("LR11x0 no TCXO Vref, XTAL only (DIO3 free as IRQ)");
|
||||
#if defined(TCXO_OPTIONAL)
|
||||
LOG_DEBUG("TCXO_OPTIONAL: oscillator type unknown, probing XTAL first and using any TCXO Vref only as fallback");
|
||||
LOG_DEBUG("TCXO_OPTIONAL: osc type unknown, probe XTAL first, TCXO Vref as fallback");
|
||||
#endif
|
||||
|
||||
RadioLibInterface::init();
|
||||
@@ -156,7 +156,7 @@ template <typename T> bool LR11x0Interface<T>::init()
|
||||
#if defined(TCXO_OPTIONAL)
|
||||
// 2. XTAL failed with the chip present, so fall back to the TCXO if the variant configured one
|
||||
if (res != RADIOLIB_ERR_NONE && res != RADIOLIB_ERR_CHIP_NOT_FOUND && tcxoVoltage > 0) {
|
||||
LOG_WARN("LR11x0 XTAL init failed (err %d), retrying with TCXO Vref %f V", res, tcxoVoltage);
|
||||
LOG_WARN("LR11x0 XTAL init failed (err %d), retry with TCXO Vref %f V", res, tcxoVoltage);
|
||||
attemptVoltage = tcxoVoltage;
|
||||
res = tryBegin(2, attemptVoltage);
|
||||
if (res == RADIOLIB_ERR_NONE)
|
||||
@@ -167,7 +167,7 @@ template <typename T> bool LR11x0Interface<T>::init()
|
||||
// 3. Some units need extra settling time, so give whichever oscillator we settled on one retry.
|
||||
// After a step 2 fallback that is a second TCXO attempt, which is where settling actually matters.
|
||||
if (lr11x0SpiFailed(res)) {
|
||||
LOG_WARN("LR11x0 init failed with %d (SPI command failure), retrying after delay...", res);
|
||||
LOG_WARN("LR11x0 init failed with %d (SPI cmd failure), retry after delay", res);
|
||||
delay(100);
|
||||
res = tryBegin(3, attemptVoltage);
|
||||
}
|
||||
@@ -179,9 +179,9 @@ template <typename T> bool LR11x0Interface<T>::init()
|
||||
#ifdef LR11X0_UPDATE_FIRMWARE_TO
|
||||
// An interrupted update leaves the radio sitting in bootloader mode, where begin() fails. Retry the
|
||||
// flash from here rather than giving up, otherwise the device could never recover on its own.
|
||||
LOG_WARN("LR11x0 did not start; attempting firmware recovery in case an update was interrupted");
|
||||
LOG_WARN("LR11x0 did not start; firmware recovery in case update was interrupted");
|
||||
if (lora.updateFirmware(lr11xx_firmware_image, LR11XX_FIRMWARE_IMAGE_SIZE, true) == RADIOLIB_ERR_NONE) {
|
||||
LOG_INFO("LR1110 firmware recovery succeeded, re-initializing radio");
|
||||
LOG_INFO("LR1110 firmware recovery OK, re-init radio");
|
||||
res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
|
||||
}
|
||||
#endif
|
||||
@@ -202,8 +202,8 @@ template <typename T> bool LR11x0Interface<T>::init()
|
||||
// One-shot transceiver firmware update, opt-in per variant. Only runs when the part is an LR1110 running
|
||||
// older firmware than the baked-in image, so once it has succeeded it is a no-op on subsequent boots.
|
||||
if (transceiverDevice == RADIOLIB_LR11X0_DEVICE_LR1110 && transceiverFw != 0 && transceiverFw < LR11X0_UPDATE_FIRMWARE_TO) {
|
||||
LOG_WARN("LR1110 transceiver FW %d.%d is older than %d.%d - updating now. DO NOT POWER OFF: this "
|
||||
"erases and rewrites the radio's own flash.",
|
||||
LOG_WARN("LR1110 transceiver FW %d.%d older than %d.%d - updating. DO NOT POWER OFF: "
|
||||
"rewrites radio's own flash",
|
||||
transceiverFw >> 8, transceiverFw & 0xFF, LR11X0_UPDATE_FIRMWARE_TO >> 8, LR11X0_UPDATE_FIRMWARE_TO & 0xFF);
|
||||
|
||||
int upd = lora.updateFirmware(lr11xx_firmware_image, LR11XX_FIRMWARE_IMAGE_SIZE, true);
|
||||
@@ -214,7 +214,7 @@ template <typename T> bool LR11x0Interface<T>::init()
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("LR1110 firmware update complete, re-initializing radio");
|
||||
LOG_INFO("LR1110 firmware update complete, re-init radio");
|
||||
res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
|
||||
if (res != RADIOLIB_ERR_NONE) {
|
||||
LOG_ERROR("LR11x0 re-init after firmware update failed %s%d", radioLibErr, res);
|
||||
@@ -259,7 +259,7 @@ template <typename T> bool LR11x0Interface<T>::init()
|
||||
LOG_INFO("Set RX gain to boosted mode; result: %d", res);
|
||||
} else {
|
||||
res = lora.setRxBoostedGainMode(false);
|
||||
LOG_INFO("Set RX gain to power saving mode (boosted mode off); result: %d", res);
|
||||
LOG_INFO("Set RX gain to power saving mode; result: %d", res);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,7 +318,7 @@ template <typename T> bool LR11x0Interface<T>::reconfigure()
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T> void LR11x0Interface<T>::disableInterrupt()
|
||||
template <typename T> void LR11x0Interface<T>::clearRadioIsr()
|
||||
{
|
||||
lora.clearIrqAction();
|
||||
}
|
||||
@@ -330,7 +330,7 @@ template <typename T> void LR11x0Interface<T>::setStandby()
|
||||
int err = lora.standby();
|
||||
|
||||
if (err != RADIOLIB_ERR_NONE) {
|
||||
LOG_DEBUG("LR11x0 standby failed with error %d", err);
|
||||
LOG_DEBUG("LR11x0 standby failed, err %d", err);
|
||||
}
|
||||
|
||||
assert(err == RADIOLIB_ERR_NONE);
|
||||
|
||||
@@ -47,12 +47,12 @@ template <class T> class LR11x0Interface : public RadioLibInterface
|
||||
/**
|
||||
* Glue functions called from ISR land
|
||||
*/
|
||||
virtual void disableInterrupt() override;
|
||||
virtual void clearRadioIsr() override;
|
||||
|
||||
/**
|
||||
* Enable a particular ISR callback glue function
|
||||
*/
|
||||
virtual void enableInterrupt(void (*callback)()) { lora.setIrqAction(callback); }
|
||||
virtual void setRadioIsr(void (*callback)()) override { lora.setIrqAction(callback); }
|
||||
|
||||
/** can we detect a LoRa preamble on the current channel? */
|
||||
virtual bool isChannelActive() override;
|
||||
|
||||
@@ -69,17 +69,17 @@ template <typename T> bool LR20x0Interface<T>::init()
|
||||
// FIXME: correct logic to default to not using TCXO if no voltage is specified for LR20x0_DIO3_TCXO_VOLTAGE
|
||||
#elif defined(LR2021_DIO3_TCXO_VOLTAGE)
|
||||
float tcxoVoltage = LR2021_DIO3_TCXO_VOLTAGE;
|
||||
LOG_DEBUG("LR2021_DIO3_TCXO_VOLTAGE defined, using DIO3 as TCXO reference voltage at %f V", LR2021_DIO3_TCXO_VOLTAGE);
|
||||
LOG_DEBUG("LR2021_DIO3_TCXO_VOLTAGE defined, DIO3 as TCXO Vref %f V", LR2021_DIO3_TCXO_VOLTAGE);
|
||||
// (DIO3 is not free to be used as an IRQ)
|
||||
#elif defined(TCXO_OPTIONAL)
|
||||
float tcxoVoltage = 1.6f; // TCXO_OPTIONAL: try default 1.6 V first, fall back to XTAL on failure
|
||||
LOG_DEBUG("TCXO_OPTIONAL: no LR2021_DIO3_TCXO_VOLTAGE defined, trying default TCXO Vref 1.6 V first");
|
||||
LOG_DEBUG("TCXO_OPTIONAL: no LR2021_DIO3_TCXO_VOLTAGE, try default TCXO Vref 1.6 V first");
|
||||
#else
|
||||
float tcxoVoltage =
|
||||
0; // "TCXO reference voltage to be set on DIO3. Defaults to 1.6 V, set to 0 to skip." per
|
||||
// https://github.com/jgromes/RadioLib/blob/690a050ebb46e6097c5d00c371e961c1caa3b52e/src/modules/LR11x0/LR11x0.h#L471C26-L471C104
|
||||
// (DIO3 is free to be used as an IRQ)
|
||||
LOG_DEBUG("LR2021_DIO3_TCXO_VOLTAGE not defined, not using DIO3 as TCXO reference voltage");
|
||||
LOG_DEBUG("LR2021_DIO3_TCXO_VOLTAGE not defined, DIO3 not used as TCXO Vref");
|
||||
#endif
|
||||
|
||||
RadioLibInterface::init();
|
||||
@@ -119,7 +119,7 @@ template <typename T> bool LR20x0Interface<T>::init()
|
||||
|
||||
// Retry if we get SPI command failed - some units need extra TCXO stabilization time
|
||||
if (res == RADIOLIB_ERR_SPI_CMD_FAILED) {
|
||||
LOG_WARN("LR20x0 init failed with %d (SPI_CMD_FAILED), retrying after delay...", res);
|
||||
LOG_WARN("LR20x0 init failed with %d (SPI_CMD_FAILED), retry after delay", res);
|
||||
delay(100);
|
||||
res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
|
||||
}
|
||||
@@ -127,7 +127,7 @@ template <typename T> bool LR20x0Interface<T>::init()
|
||||
#if defined(TCXO_OPTIONAL)
|
||||
// If init failed for any reason other than chip not found, retry without TCXO (XTAL mode)
|
||||
if (res != RADIOLIB_ERR_NONE && res != RADIOLIB_ERR_CHIP_NOT_FOUND && tcxoVoltage > 0) {
|
||||
LOG_WARN("LR20x0 init failed with TCXO Vref %f V (err %d), retrying without TCXO", tcxoVoltage, res);
|
||||
LOG_WARN("LR20x0 init failed with TCXO Vref %f V (err %d), retry without TCXO", tcxoVoltage, res);
|
||||
tcxoVoltage = 0;
|
||||
res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
|
||||
if (res == RADIOLIB_ERR_NONE)
|
||||
@@ -166,7 +166,7 @@ template <typename T> bool LR20x0Interface<T>::init()
|
||||
LOG_INFO("Set RX gain to boosted mode; result: %d", res);
|
||||
} else {
|
||||
res = lora.setRxBoostedGainMode(false);
|
||||
LOG_INFO("Set RX gain to power saving mode (boosted mode off); result: %d", res);
|
||||
LOG_INFO("Set RX gain to power saving mode; result: %d", res);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,7 +220,7 @@ template <typename T> bool LR20x0Interface<T>::reconfigure()
|
||||
|
||||
int res = lora.begin(freq, bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
|
||||
if (res == RADIOLIB_ERR_SPI_CMD_FAILED) {
|
||||
LOG_WARN("LR20x0 band-hop begin SPI_CMD_FAILED, retrying...");
|
||||
LOG_WARN("LR20x0 band-hop begin SPI_CMD_FAILED, retrying");
|
||||
delay(100);
|
||||
res = lora.begin(freq, bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
|
||||
}
|
||||
@@ -323,7 +323,7 @@ template <typename T> bool LR20x0Interface<T>::reconfigure()
|
||||
return success;
|
||||
}
|
||||
|
||||
template <typename T> void LR20x0Interface<T>::disableInterrupt()
|
||||
template <typename T> void LR20x0Interface<T>::clearRadioIsr()
|
||||
{
|
||||
lora.clearIrqAction();
|
||||
}
|
||||
@@ -335,7 +335,7 @@ template <typename T> void LR20x0Interface<T>::setStandby()
|
||||
int err = lora.standby();
|
||||
|
||||
if (err != RADIOLIB_ERR_NONE) {
|
||||
LOG_DEBUG("LR20x0 standby failed with error %d", err);
|
||||
LOG_DEBUG("LR20x0 standby failed, err %d", err);
|
||||
}
|
||||
|
||||
assert(err == RADIOLIB_ERR_NONE);
|
||||
|
||||
@@ -42,12 +42,12 @@ template <class T> class LR20x0Interface : public RadioLibInterface
|
||||
/**
|
||||
* Glue functions called from ISR land
|
||||
*/
|
||||
virtual void disableInterrupt() override;
|
||||
virtual void clearRadioIsr() override;
|
||||
|
||||
/**
|
||||
* Enable a particular ISR callback glue function
|
||||
*/
|
||||
virtual void enableInterrupt(void (*callback)()) { lora.setIrqAction(callback); }
|
||||
virtual void setRadioIsr(void (*callback)()) override { lora.setIrqAction(callback); }
|
||||
|
||||
/** can we detect a LoRa preamble on the current channel? */
|
||||
virtual bool isChannelActive() override;
|
||||
|
||||
@@ -115,7 +115,7 @@ template <class T> class MemoryDynamic : public Allocator<T>
|
||||
{
|
||||
T *p = (T *)malloc(sizeof(T));
|
||||
if (!p) {
|
||||
LOG_WARN("malloc(%u) failed, heap exhausted!", (unsigned)sizeof(T));
|
||||
LOG_WARN("malloc(%u) failed, heap exhausted", (unsigned)sizeof(T));
|
||||
return nullptr;
|
||||
}
|
||||
this->auditAdd((int32_t)sizeof(T));
|
||||
@@ -156,7 +156,7 @@ template <class T, int MaxSize> class MemoryPool : public Allocator<T>
|
||||
this->auditAdd(-(int32_t)sizeof(T));
|
||||
LOG_HEAP("Released static pool item %d at 0x%x", index, p);
|
||||
} else {
|
||||
LOG_WARN("Pointer 0x%x not from our pool!", p);
|
||||
LOG_WARN("Pointer 0x%x not from our pool", p);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ template <class T, int MaxSize> class MemoryPool : public Allocator<T>
|
||||
}
|
||||
|
||||
// No free slots available - return nullptr instead of asserting
|
||||
LOG_WARN("No free slots available in static memory pool!");
|
||||
LOG_WARN("No free slots available in static memory pool");
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
@@ -170,7 +170,7 @@ void MeshModule::callModules(meshtastic_MeshPacket &mp, RxSource src)
|
||||
pi.sendResponse(mp);
|
||||
LOG_INFO("Asked module '%s' to send a response", pi.name);
|
||||
} else {
|
||||
LOG_DEBUG("Module '%s' cannot respond on portnum=%d", pi.name, mp.decoded.portnum);
|
||||
LOG_DEBUG("Module '%s' can't respond on portnum=%d", pi.name, mp.decoded.portnum);
|
||||
}
|
||||
ignoreRequest = ignoreRequest || pi.ignoreRequest; // If at least one module asks it, we may ignore a request
|
||||
} else {
|
||||
|
||||
+33
-33
@@ -13,6 +13,7 @@
|
||||
#include "PowerFSM.h"
|
||||
#include "TypeConversions.h"
|
||||
#include "UptimeClock.h"
|
||||
#include "gps/GPSLog.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "graphics/draw/MessageRenderer.h"
|
||||
#include "main.h"
|
||||
@@ -95,7 +96,7 @@ int MeshService::handleFromRadio(const meshtastic_MeshPacket *mp)
|
||||
meshtastic_Config_DeviceConfig_Role_CLIENT_BASE);
|
||||
if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
|
||||
mp->decoded.portnum == meshtastic_PortNum_TELEMETRY_APP && mp->decoded.request_id > 0) {
|
||||
LOG_DEBUG("Received telemetry response. Skip sending our NodeInfo");
|
||||
LOG_DEBUG("Got telemetry response. Skip our NodeInfo");
|
||||
// ignore our request for its NodeInfo
|
||||
} else if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
|
||||
!nodeInfoLiteHasUser(nodeDB->getMeshNode(mp->from)) && nodeInfoModule && !isPreferredRebroadcaster &&
|
||||
@@ -103,13 +104,13 @@ int MeshService::handleFromRadio(const meshtastic_MeshPacket *mp)
|
||||
if (airTime->isTxAllowedChannelUtil(true)) {
|
||||
const int8_t hopsUsed = getHopsAway(*mp, config.lora.hop_limit);
|
||||
if (hopsUsed > (int32_t)(config.lora.hop_limit + 2)) {
|
||||
LOG_DEBUG("Skip send NodeInfo: %d hops away is too far away", hopsUsed);
|
||||
LOG_DEBUG("Skip send NodeInfo: %d hops too far", hopsUsed);
|
||||
} else {
|
||||
LOG_INFO("Heard new node on ch. %d, send NodeInfo and ask for response", mp->channel);
|
||||
LOG_INFO("Heard new node on ch. %d, send NodeInfo, ask response", mp->channel);
|
||||
nodeInfoModule->sendOurNodeInfo(mp->from, true, mp->channel);
|
||||
}
|
||||
} else {
|
||||
LOG_DEBUG("Skip sending NodeInfo > 25%% ch. util");
|
||||
LOG_DEBUG("Skip NodeInfo > 25%% ch. util");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,14 +182,14 @@ NodeNum MeshService::getNodenumFromRequestId(uint32_t request_id)
|
||||
return nodenum;
|
||||
}
|
||||
|
||||
// Back-calculate the real epoch for any queued packet still carrying a millis() rx_time
|
||||
// Back-calculate the real epoch for any queued packet still carrying an uptime-seconds 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();
|
||||
const uint32_t nowUptimeSecs = Time::getUptimeSecs();
|
||||
|
||||
// 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.
|
||||
@@ -197,14 +198,16 @@ void MeshService::reconcilePendingRxTimes()
|
||||
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;
|
||||
// Both stamps are monotonic uptime seconds, so the elapsed term is exact at any age.
|
||||
// If it somehow exceeds the epoch, leave the packet un-dated rather than pre-1970.
|
||||
const uint32_t elapsedSecs = nowUptimeSecs - p->rx_time;
|
||||
if (elapsedSecs < nowEpoch) {
|
||||
p->rx_time = nowEpoch - elapsedSecs;
|
||||
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!");
|
||||
LOG_CRIT("Requeue to toPhoneQueue failed");
|
||||
releaseToPool(p);
|
||||
fromNum++; // notify observers so the phone can resync
|
||||
}
|
||||
@@ -232,14 +235,14 @@ void MeshService::injectAsReceived(meshtastic_MeshPacket &p)
|
||||
p.decoded.portnum = scratch.portnum;
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR("inject: could not decode Compressed envelope, dropping");
|
||||
LOG_ERROR("inject: can't decode Compressed envelope, drop");
|
||||
return;
|
||||
}
|
||||
}
|
||||
// The real RX path (RadioLibInterface::handleReceiveInterrupt) drops sender==0; mirror it so injection
|
||||
// behaves identically to an over-the-air frame.
|
||||
if (p.from == 0) {
|
||||
LOG_WARN("inject: dropping frame with from==0 (matches real LoRa RX)");
|
||||
LOG_WARN("inject: drop frame with from==0 (matches real LoRa RX)");
|
||||
return;
|
||||
}
|
||||
meshtastic_MeshPacket *mp = packetPool.allocCopy(p);
|
||||
@@ -341,7 +344,7 @@ ErrorCode MeshService::sendQueueStatusToPhone(const meshtastic_QueueStatus &qs,
|
||||
copied->mesh_packet_id = mesh_packet_id;
|
||||
|
||||
if (toPhoneQueueStatusQueue.numFree() == 0) {
|
||||
LOG_INFO("tophone queue status queue is full, discard oldest");
|
||||
LOG_INFO("tophone queue status queue full, discard oldest");
|
||||
meshtastic_QueueStatus *d = toPhoneQueueStatusQueue.dequeuePtr(0);
|
||||
if (d)
|
||||
releaseQueueStatusToPool(d);
|
||||
@@ -421,9 +424,8 @@ bool MeshService::trySendPosition(NodeNum dest, bool wantReplies)
|
||||
if (!found) {
|
||||
// No channel with position enabled: fall back to sending nodeinfo, as before.
|
||||
if (nodeInfoModule) {
|
||||
LOG_INFO(
|
||||
"No channel with position enabled; sending nodeinfo instead to 0x%08x, wantReplies=%d, channel=%d",
|
||||
dest, wantReplies, node->channel);
|
||||
LOG_INFO("No position-enabled channel; send nodeinfo instead to 0x%08x, wantReplies=%d, channel=%d", dest,
|
||||
wantReplies, node->channel);
|
||||
nodeInfoModule->sendOurNodeInfo(dest, wantReplies, node->channel);
|
||||
}
|
||||
return false;
|
||||
@@ -470,7 +472,7 @@ void MeshService::sendToPhone(meshtastic_MeshPacket *p)
|
||||
// Withhold decoded nested payloads a strict phone decoder would reject; still-encrypted packets
|
||||
// pass through (the phone may hold the key).
|
||||
if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && !phonePayloadIsDecodable(p->decoded)) {
|
||||
LOG_WARN("Dropping undecodable portnum=%d payload from phone delivery (from=0x%08x)", p->decoded.portnum, p->from);
|
||||
LOG_WARN("Drop undecodable portnum=%d payload from phone delivery (from=0x%08x)", p->decoded.portnum, p->from);
|
||||
releaseToPool(p);
|
||||
fromNum++; // notify observers so the phone can resync
|
||||
return;
|
||||
@@ -490,12 +492,12 @@ void MeshService::sendToPhone(meshtastic_MeshPacket *p)
|
||||
if (toPhoneQueue.numFree() == 0) {
|
||||
if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP ||
|
||||
p->decoded.portnum == meshtastic_PortNum_RANGE_TEST_APP) {
|
||||
LOG_WARN("ToPhone queue is full, discard oldest");
|
||||
LOG_WARN("ToPhone queue full, discard oldest");
|
||||
meshtastic_MeshPacket *d = toPhoneQueue.dequeuePtr(0);
|
||||
if (d)
|
||||
releaseToPool(d);
|
||||
} else {
|
||||
LOG_WARN("ToPhone queue is full, drop packet");
|
||||
LOG_WARN("ToPhone queue full, drop packet");
|
||||
releaseToPool(p);
|
||||
fromNum++; // Make sure to notify observers in case they are reconnected so they can get the packets
|
||||
return;
|
||||
@@ -503,7 +505,7 @@ void MeshService::sendToPhone(meshtastic_MeshPacket *p)
|
||||
}
|
||||
|
||||
if (toPhoneQueue.enqueue(p, 0) == false) {
|
||||
LOG_CRIT("Failed to queue a packet into toPhoneQueue!");
|
||||
LOG_CRIT("Queue to toPhoneQueue failed");
|
||||
releaseToPool(p);
|
||||
fromNum++; // notify observers so phone can resync
|
||||
return;
|
||||
@@ -513,16 +515,16 @@ void MeshService::sendToPhone(meshtastic_MeshPacket *p)
|
||||
|
||||
void MeshService::sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage *m)
|
||||
{
|
||||
LOG_DEBUG("Send mqtt message on topic '%s' to client for proxy", m->topic);
|
||||
LOG_DEBUG("Send mqtt msg on topic '%s' to proxy client", m->topic);
|
||||
if (toPhoneMqttProxyQueue.numFree() == 0) {
|
||||
LOG_WARN("MqttClientProxyMessagePool queue is full, discard oldest");
|
||||
LOG_WARN("MqttClientProxyMessagePool queue full, discard oldest");
|
||||
meshtastic_MqttClientProxyMessage *d = toPhoneMqttProxyQueue.dequeuePtr(0);
|
||||
if (d)
|
||||
releaseMqttClientProxyMessageToPool(d);
|
||||
}
|
||||
|
||||
if (toPhoneMqttProxyQueue.enqueue(m, 0) == false) {
|
||||
LOG_CRIT("Failed to queue a packet into toPhoneMqttProxyQueue!");
|
||||
LOG_CRIT("Queue to toPhoneMqttProxyQueue failed");
|
||||
releaseMqttClientProxyMessageToPool(m);
|
||||
return;
|
||||
}
|
||||
@@ -532,7 +534,7 @@ void MeshService::sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage
|
||||
void MeshService::sendRoutingErrorResponse(meshtastic_Routing_Error error, const meshtastic_MeshPacket *mp)
|
||||
{
|
||||
if (!mp) {
|
||||
LOG_WARN("Cannot send routing error response: null packet");
|
||||
LOG_WARN("Can't send routing error response: null packet");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -540,7 +542,7 @@ void MeshService::sendRoutingErrorResponse(meshtastic_Routing_Error error, const
|
||||
if (routingModule) {
|
||||
routingModule->sendAckNak(error, mp->from, mp->id, mp->channel);
|
||||
} else {
|
||||
LOG_ERROR("Cannot send routing error response: no routing module");
|
||||
LOG_ERROR("Can't send routing error response: no routing module");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -548,14 +550,14 @@ void MeshService::sendClientNotification(meshtastic_ClientNotification *n)
|
||||
{
|
||||
LOG_DEBUG("Send client notification to phone");
|
||||
if (toPhoneClientNotificationQueue.numFree() == 0) {
|
||||
LOG_WARN("ClientNotification queue is full, discard oldest");
|
||||
LOG_WARN("ClientNotification queue full, discard oldest");
|
||||
meshtastic_ClientNotification *d = toPhoneClientNotificationQueue.dequeuePtr(0);
|
||||
if (d)
|
||||
releaseClientNotificationToPool(d);
|
||||
}
|
||||
|
||||
if (toPhoneClientNotificationQueue.enqueue(n, 0) == false) {
|
||||
LOG_CRIT("Failed to queue a notification into toPhoneClientNotificationQueue!");
|
||||
LOG_CRIT("Queue to toPhoneClientNotificationQueue failed");
|
||||
releaseClientNotificationToPool(n);
|
||||
return;
|
||||
}
|
||||
@@ -597,9 +599,7 @@ int MeshService::onGPSChanged(const meshtastic::GPSStatus *newStatus)
|
||||
pos = gps->p;
|
||||
} else {
|
||||
// The GPS has lost lock
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG("onGPSchanged() - lost validLocation");
|
||||
#endif
|
||||
LOG_DEBUG_GPS("onGPSchanged() - lost validLocation");
|
||||
}
|
||||
// Used fixed position if configured regardless of GPS lock
|
||||
if (config.position.fixed_position) {
|
||||
@@ -629,7 +629,7 @@ bool MeshService::isToPhoneQueueEmpty()
|
||||
|
||||
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
|
||||
// rx_time may be an uptime-seconds 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;
|
||||
|
||||
@@ -137,8 +137,8 @@ class MeshService
|
||||
// search the queue for a request id and return the matching nodenum
|
||||
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.
|
||||
// Rewrite any queued-for-phone packet still carrying an uptime-seconds rx_time placeholder
|
||||
// into a real epoch, now that the wall clock is trustworthy.
|
||||
void reconcilePendingRxTimes();
|
||||
|
||||
// Release QueueStatus packet to pool
|
||||
|
||||
+32
-18
@@ -1,6 +1,8 @@
|
||||
#include "NextHopRouter.h"
|
||||
#include "Default.h"
|
||||
#include "MeshTypes.h"
|
||||
#include "Throttle.h"
|
||||
#include "UptimeClock.h"
|
||||
#include "meshUtils.h"
|
||||
#if !MESHTASTIC_EXCLUDE_TRACEROUTE
|
||||
#include "modules/TraceRouteModule.h"
|
||||
@@ -67,7 +69,7 @@ ErrorCode NextHopRouter::send(meshtastic_MeshPacket *p)
|
||||
wasSeenRecently(p); // FIXME, move this to a sniffSent method
|
||||
|
||||
p->next_hop = getNextHop(p->to, p->relay_node).value_or(NO_NEXT_HOP_PREFERENCE); // set the next hop
|
||||
LOG_DEBUG("Setting next hop for packet with dest %x to %x", p->to, p->next_hop);
|
||||
LOG_TRACE("Set next hop for dest 0x%08x to 0x%x", p->to, p->next_hop);
|
||||
|
||||
// If it's from us, ReliableRouter already handles retransmissions if want_ack is set. If a next hop is set and hop limit is
|
||||
// not 0 or want_ack is set, start retransmissions
|
||||
@@ -113,7 +115,8 @@ bool NextHopRouter::shouldFilterReceived(const meshtastic_MeshPacket *p)
|
||||
// If repeated and not in Tx queue anymore, try relaying again, or if we are the destination, send the ACK again
|
||||
if (isRepeated) {
|
||||
if (!findInTxQueue(p->from, p->id)) {
|
||||
if (reprocessPacket(p) && !perhapsRebroadcast(p) && isToUs(p) && p->want_ack) {
|
||||
if (reprocessPacket(p) && !isBlockedEventCoordinatePacket(p) && !perhapsRebroadcast(p) && isToUs(p) &&
|
||||
p->want_ack) {
|
||||
sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0);
|
||||
}
|
||||
}
|
||||
@@ -157,7 +160,7 @@ void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtast
|
||||
// -> store nothing and keep flooding (safe).
|
||||
if (nodeDB->resolveUniqueLastByte(p->relay_node, /*requireDirectNeighbor=*/false)) {
|
||||
if (origTx && origTx->next_hop != p->relay_node) { // Not already set
|
||||
LOG_INFO("Update next hop of 0x%08x to 0x%x based on ACK/reply (was relayer %d we were sole %d)", p->from,
|
||||
LOG_INFO("Update next hop of 0x%08x to 0x%x from ACK/reply (was relayer %d we were sole %d)", p->from,
|
||||
p->relay_node, wasAlreadyRelayer, weWereSoleRelayer);
|
||||
origTx->next_hop = p->relay_node;
|
||||
}
|
||||
@@ -190,6 +193,14 @@ void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtast
|
||||
/* Check if we should be rebroadcasting this packet if so, do so. */
|
||||
bool NextHopRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p)
|
||||
{
|
||||
#if USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL
|
||||
// Never relay coordinate-bearing packets on the event ("everyone") channel.
|
||||
// Closes the reliable-retransmit-dupe path that runs before handleReceived().
|
||||
if (isBlockedEventCoordinatePacket(p)) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Check if traffic management wants to exhaust this packet's hops
|
||||
bool exhaustHops = false;
|
||||
#if HAS_TRAFFIC_MANAGEMENT
|
||||
@@ -214,17 +225,17 @@ bool NextHopRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p)
|
||||
meshtastic_MeshPacket *tosend = packetPool.allocCopy(*p); // keep a copy because we will be sending it
|
||||
if (!tosend)
|
||||
return true;
|
||||
LOG_INFO("Rebroadcast received message coming from %x", p->relay_node);
|
||||
LOG_INFO("Rebroadcast msg from %x", p->relay_node);
|
||||
|
||||
// If exhausting hops, force hop_limit = 0 regardless of other logic
|
||||
if (exhaustHops) {
|
||||
tosend->hop_limit = 0;
|
||||
LOG_INFO("Traffic management: exhausting hops for 0x%08x, setting hop_limit=0", getFrom(p));
|
||||
LOG_INFO("Traffic management: exhaust hops for 0x%08x, hop_limit=0", getFrom(p));
|
||||
} else if (shouldDecrementHopLimit(p)) {
|
||||
// Use shared logic to determine if hop_limit should be decremented
|
||||
tosend->hop_limit--; // bump down the hop count
|
||||
} else {
|
||||
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: keep hop_limit");
|
||||
}
|
||||
#if USERPREFS_EVENT_MODE
|
||||
capEventRelayHops(tosend);
|
||||
@@ -266,7 +277,7 @@ std::optional<uint8_t> NextHopRouter::getNextHop(NodeNum to, uint8_t relay_node)
|
||||
// TraceRouteModule) with no matching record is left authoritative.
|
||||
const RouteHealth *h = findRouteHealth(to);
|
||||
if (h && h->lastNextHop == node->next_hop && isRouteStale(*h, millis())) {
|
||||
LOG_INFO("Next hop 0x%x for 0x%08x is stale (age/fails); flood and clear", node->next_hop, to);
|
||||
LOG_INFO("Next hop 0x%x for 0x%08x stale (age/fails); flood and clear", node->next_hop, to);
|
||||
node->next_hop = NO_NEXT_HOP_PREFERENCE; // clear persisted route
|
||||
clearRouteHealth(to); // clear RAM health
|
||||
return std::nullopt;
|
||||
@@ -298,14 +309,14 @@ std::optional<uint8_t> NextHopRouter::getNextHop(NodeNum to, uint8_t relay_node)
|
||||
if (hint && hint != relay_node) {
|
||||
const RouteHealth *h = findRouteHealth(to);
|
||||
if (h && h->lastNextHop == hint && isRouteStale(*h, millis())) {
|
||||
LOG_INFO("TMM next hop 0x%x for 0x%08x is stale (age/fails); flood and clear", hint, to);
|
||||
LOG_INFO("TMM next hop 0x%x for 0x%08x stale (age/fails); flood and clear", hint, to);
|
||||
trafficManagementModule->clearNextHop(to); // clear overflow route (setNextHop won't store 0)
|
||||
clearRouteHealth(to); // clear RAM health
|
||||
return std::nullopt;
|
||||
}
|
||||
ResolvedNode r = nodeDB->resolveLastByte(hint, /*requireDirectNeighbor=*/true);
|
||||
if (r.status == LastByteResolution::Unique) {
|
||||
LOG_DEBUG("Next hop for 0x%08x is 0x%x (TMM cache)", to, hint);
|
||||
LOG_TRACE("Next hop for 0x%08x is 0x%x (TMM cache)", to, hint);
|
||||
return hint;
|
||||
}
|
||||
LOG_WARN("TMM next hop 0x%x for 0x%08x %s; set no pref", hint, to,
|
||||
@@ -394,7 +405,9 @@ PendingPacket *NextHopRouter::startRetransmission(meshtastic_MeshPacket *p, uint
|
||||
*/
|
||||
int32_t NextHopRouter::doRetransmissions()
|
||||
{
|
||||
uint32_t now = millis();
|
||||
// Same clock Throttle reads, so setNextTx() deadlines and this test can't diverge under an
|
||||
// injected test clock.
|
||||
uint32_t now = Time::getMillis();
|
||||
int32_t d = INT32_MAX;
|
||||
|
||||
// FIXME, we should use a better datastructure rather than walking through this map.
|
||||
@@ -405,19 +418,20 @@ int32_t NextHopRouter::doRetransmissions()
|
||||
|
||||
bool stillValid = true; // assume we'll keep this record around
|
||||
|
||||
// FIXME, handle 51 day rolloever here!!!
|
||||
if (p.nextTxMsec <= now) {
|
||||
// Judged against the snapshot above, so one pass sees one instant and the 49.7 day wrap
|
||||
// can't stall retransmission.
|
||||
if (Throttle::deadlinePassedAt(now, p.nextTxMsec)) {
|
||||
if (p.numRetransmissions == 0) {
|
||||
if (isFromUs(p.packet)) {
|
||||
LOG_DEBUG("Reliable send failed, returning a nak for fr=0x%08x,to=0x%08x,id=0x%08x", p.packet->from,
|
||||
p.packet->to, p.packet->id);
|
||||
LOG_DEBUG("Reliable send failed, return nak fr=0x%08x,to=0x%08x,id=0x%08x", p.packet->from, p.packet->to,
|
||||
p.packet->id);
|
||||
sendAckNak(meshtastic_Routing_Error_MAX_RETRANSMIT, getFrom(p.packet), p.packet->id, p.packet->channel);
|
||||
}
|
||||
// Note: we don't stop retransmission here, instead the Nak packet gets processed in sniffReceived
|
||||
stopRetransmission(it->first);
|
||||
stillValid = false; // just deleted it
|
||||
} else {
|
||||
LOG_DEBUG("Sending retransmission fr=0x%08x,to=0x%08x,id=0x%08x, tries left=%d", p.packet->from, p.packet->to,
|
||||
LOG_DEBUG("Send retransmission fr=0x%08x,to=0x%08x,id=0x%08x, tries left=%d", p.packet->from, p.packet->to,
|
||||
p.packet->id, p.numRetransmissions);
|
||||
|
||||
if (!isBroadcast(p.packet->to)) {
|
||||
@@ -430,7 +444,7 @@ int32_t NextHopRouter::doRetransmissions()
|
||||
// Also reset it in the nodeDB
|
||||
meshtastic_NodeInfoLite *sentTo = nodeDB->getMeshNode(p.packet->to);
|
||||
if (sentTo) {
|
||||
LOG_INFO("Resetting next hop for packet with dest 0x%08x", p.packet->to);
|
||||
LOG_INFO("Reset next hop for dest 0x%08x", p.packet->to);
|
||||
sentTo->next_hop = NO_NEXT_HOP_PREFERENCE;
|
||||
}
|
||||
#if HAS_TRAFFIC_MANAGEMENT
|
||||
@@ -502,8 +516,8 @@ void NextHopRouter::setNextTx(PendingPacket *pending)
|
||||
{
|
||||
assert(iface);
|
||||
auto d = iface->getRetransmissionMsec(pending->packet);
|
||||
pending->nextTxMsec = millis() + d;
|
||||
LOG_DEBUG("Setting next retransmission in %u msecs: ", d);
|
||||
pending->nextTxMsec = Time::getMillis() + d;
|
||||
LOG_TRACE("Next retransmission in %u msecs", d);
|
||||
printPacket("", pending->packet);
|
||||
setReceivedMessage(); // Run ASAP, so we can figure out our correct sleep time
|
||||
}
|
||||
|
||||
+200
-99
@@ -19,6 +19,7 @@
|
||||
#include "SafeFile.h"
|
||||
#include "TransmitHistory.h"
|
||||
#include "TypeConversions.h"
|
||||
#include "UptimeClock.h"
|
||||
#include "error.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "main.h"
|
||||
@@ -98,11 +99,13 @@ static unsigned char userprefs_admin_key_2[] = USERPREFS_USE_ADMIN_KEY_2;
|
||||
|
||||
// Weak empty variant initialization function.
|
||||
// May be redefined by variant files.
|
||||
void variantDefaultConfig() __attribute__((weak));
|
||||
void variantDefaultConfig() {}
|
||||
// noinline: weak default and call site share this TU, so LTO would inline the empty body and
|
||||
// never link the variant's strong override. Same guard as earlyInitVariant() in main.cpp.
|
||||
__attribute__((noinline)) void variantDefaultConfig() __attribute__((weak));
|
||||
__attribute__((noinline)) void variantDefaultConfig() {}
|
||||
|
||||
void variantDefaultModuleConfig() __attribute__((weak));
|
||||
void variantDefaultModuleConfig() {}
|
||||
__attribute__((noinline)) void variantDefaultModuleConfig() __attribute__((weak));
|
||||
__attribute__((noinline)) void variantDefaultModuleConfig() {}
|
||||
|
||||
#ifdef HELTEC_MESH_NODE_T114
|
||||
|
||||
@@ -486,12 +489,12 @@ NodeDB::NodeDB()
|
||||
preferences.begin("meshtastic", false);
|
||||
myNodeInfo.reboot_count = preferences.getUInt("rebootCounter", 0);
|
||||
preferences.end();
|
||||
LOG_DEBUG("Number of Device Reboots: %d", myNodeInfo.reboot_count);
|
||||
LOG_DEBUG("Device reboots: %d", myNodeInfo.reboot_count);
|
||||
#endif
|
||||
|
||||
// UA_868 is obsolete; migrate to EU_868 before resetRadioConfig() below validates the region.
|
||||
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UA_868) {
|
||||
LOG_INFO("UA_868 region is obsolete, migrating saved config to EU_868");
|
||||
LOG_INFO("UA_868 obsolete, migrating config to EU_868");
|
||||
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
||||
}
|
||||
|
||||
@@ -504,7 +507,7 @@ NodeDB::NodeDB()
|
||||
// If we are setup to broadcast on any default channel slot (with default frequency slot semantics),
|
||||
// ensure that the telemetry intervals are coerced to the role-aware minimum value.
|
||||
if (channels.hasDefaultChannel()) {
|
||||
LOG_DEBUG("Coerce telemetry to role-aware minimum on defaults");
|
||||
LOG_DEBUG("Coerce telemetry to role-aware min on defaults");
|
||||
moduleConfig.telemetry.device_update_interval = Default::getConfiguredOrMinimumValue(
|
||||
moduleConfig.telemetry.device_update_interval, min_default_telemetry_interval_secs);
|
||||
moduleConfig.telemetry.environment_update_interval = Default::getConfiguredOrMinimumValue(
|
||||
@@ -527,7 +530,7 @@ NodeDB::NodeDB()
|
||||
}
|
||||
}
|
||||
if (positionUsesDefaultChannel) {
|
||||
LOG_DEBUG("Coerce position broadcasts to role-aware minimum and smart broadcast min of 5 minutes on defaults");
|
||||
LOG_DEBUG("Coerce position broadcasts to role-aware min and smart broadcast min of 5 min on defaults");
|
||||
config.position.position_broadcast_secs =
|
||||
Default::getConfiguredOrMinimumValue(config.position.position_broadcast_secs, min_default_broadcast_interval_secs);
|
||||
config.position.broadcast_smart_minimum_interval_secs = Default::getConfiguredOrMinimumValue(
|
||||
@@ -738,7 +741,7 @@ void NodeDB::resetRadioConfig(bool is_fresh_install)
|
||||
}
|
||||
|
||||
if (channelFile.channels_count != MAX_NUM_CHANNELS) {
|
||||
LOG_INFO("Set default channel and radio preferences!");
|
||||
LOG_INFO("Set default channel and radio prefs");
|
||||
|
||||
channels.initDefaults();
|
||||
// Defaults ship the public PSK, so strip it again before onConfigChanged() publishes hashes;
|
||||
@@ -755,14 +758,14 @@ void NodeDB::resetRadioConfig(bool is_fresh_install)
|
||||
|
||||
bool NodeDB::factoryReset(bool eraseBleBonds)
|
||||
{
|
||||
LOG_INFO("Perform factory reset!");
|
||||
LOG_INFO("Factory reset");
|
||||
// first, remove the "/prefs" (this removes most prefs)
|
||||
spiLock->lock();
|
||||
rmDir("/prefs"); // this uses spilock internally...
|
||||
|
||||
#ifdef FSCom
|
||||
if (FSCom.exists("/static/rangetest.csv") && !FSCom.remove("/static/rangetest.csv")) {
|
||||
LOG_ERROR("Could not remove rangetest.csv file");
|
||||
LOG_ERROR("Can't remove rangetest.csv");
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -806,7 +809,7 @@ bool NodeDB::factoryReset(bool eraseBleBonds)
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_NRF52
|
||||
LOG_INFO("Clear bluetooth bonds!");
|
||||
LOG_INFO("Clear bluetooth bonds");
|
||||
bond_print_list(BLE_GAP_ROLE_PERIPH);
|
||||
bond_print_list(BLE_GAP_ROLE_CENTRAL);
|
||||
Bluefruit.Periph.clearBonds();
|
||||
@@ -884,7 +887,7 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
|
||||
// Restrict ROUTER*, LOST AND FOUND roles for security reasons
|
||||
if (IS_ONE_OF(USERPREFS_CONFIG_DEVICE_ROLE, meshtastic_Config_DeviceConfig_Role_ROUTER,
|
||||
meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND)) {
|
||||
LOG_WARN("ROUTER roles are restricted, falling back to CLIENT role");
|
||||
LOG_WARN("ROUTER roles restricted, fall back to CLIENT");
|
||||
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
|
||||
} else {
|
||||
config.device.role = USERPREFS_CONFIG_DEVICE_ROLE;
|
||||
@@ -983,7 +986,8 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
|
||||
if (shouldPreserveKey) {
|
||||
config.security.private_key.size = 32;
|
||||
memcpy(config.security.private_key.bytes, private_key_temp, config.security.private_key.size);
|
||||
printBytes("Restored key", config.security.private_key.bytes, config.security.private_key.size);
|
||||
// Never log the key bytes: debug logs get pasted into public bug reports.
|
||||
LOG_DEBUG("Restored preserved private key");
|
||||
} else {
|
||||
config.security.private_key.size = 0;
|
||||
}
|
||||
@@ -1628,7 +1632,7 @@ void NodeDB::resetNodes(bool keepFavorites)
|
||||
NodeNum ourNum = getNodeNum();
|
||||
numMeshNodes = 1;
|
||||
if (keepFavorites) {
|
||||
LOG_INFO("Clearing node database - preserving favorites");
|
||||
LOG_INFO("Clear node database, keep favorites");
|
||||
// Compact favorites into contiguous low slots: zeroing in place leaves one above
|
||||
// numMeshNodes, invisible to every `i < numMeshNodes` scan yet still serialized to flash.
|
||||
for (size_t i = 1; i < meshNodes->size(); i++) {
|
||||
@@ -1643,7 +1647,7 @@ void NodeDB::resetNodes(bool keepFavorites)
|
||||
}
|
||||
std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.end(), meshtastic_NodeInfoLite());
|
||||
} else {
|
||||
LOG_INFO("Clearing node database - removing favorites");
|
||||
LOG_INFO("Clear node database, remove favorites");
|
||||
for (size_t i = 1; i < meshNodes->size(); i++) {
|
||||
const NodeNum gone = meshNodes->at(i).num;
|
||||
if (gone)
|
||||
@@ -1699,7 +1703,7 @@ void NodeDB::removeNodeByNum(NodeNum nodeNum)
|
||||
trafficManagementModule->purgeNode(nodeNum);
|
||||
#endif
|
||||
|
||||
LOG_DEBUG("NodeDB::removeNodeByNum purged %d entries. Save changes", removed);
|
||||
LOG_DEBUG("NodeDB::removeNodeByNum purged %d entries, saving", removed);
|
||||
saveNodeDatabaseToDisk();
|
||||
}
|
||||
|
||||
@@ -2041,7 +2045,7 @@ void NodeDB::pickNewNodeNum()
|
||||
(nodeNum == NODENUM_BROADCAST || nodeNum < NUM_RESERVED)) {
|
||||
NodeNum candidate = random(NUM_RESERVED, LONG_MAX); // try a new random choice
|
||||
if (found)
|
||||
LOG_WARN("NOTE! Our desired nodenum 0x%08x is invalid or in use, picking 0x%08x", nodeNum, candidate);
|
||||
LOG_WARN("NOTE! Desired nodenum 0x%08x invalid or in use, picking 0x%08x", nodeNum, candidate);
|
||||
nodeNum = candidate;
|
||||
}
|
||||
LOG_DEBUG("Use nodenum 0x%08x ", nodeNum);
|
||||
@@ -2073,11 +2077,11 @@ LoadFileResult NodeDB::loadProto(const char *filename, size_t protoSize, size_t
|
||||
if (fields != &meshtastic_NodeDatabase_msg)
|
||||
memset(dest_struct, 0, objSize);
|
||||
if (!pb_decode(&stream, fields, dest_struct)) {
|
||||
LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream));
|
||||
LOG_ERROR("Can't decode protobuf %s", PB_GET_ERROR(&stream));
|
||||
state = LoadFileResult::DECODE_FAILED;
|
||||
storageCorruptThisLoad = true;
|
||||
} else {
|
||||
LOG_INFO("Loaded encrypted %s successfully", filename);
|
||||
LOG_INFO("Loaded encrypted %s", filename);
|
||||
state = LoadFileResult::LOAD_SUCCESS;
|
||||
}
|
||||
} else {
|
||||
@@ -2101,18 +2105,18 @@ LoadFileResult NodeDB::loadProto(const char *filename, size_t protoSize, size_t
|
||||
fields != &meshtastic_NodeDatabase_Legacy_msg) // both NodeDatabase descriptors contain std::vector members
|
||||
memset(dest_struct, 0, objSize);
|
||||
if (!pb_decode(&stream, fields, dest_struct)) {
|
||||
LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream));
|
||||
LOG_ERROR("Can't decode protobuf %s", PB_GET_ERROR(&stream));
|
||||
state = LoadFileResult::DECODE_FAILED;
|
||||
} else {
|
||||
LOG_INFO("Loaded %s successfully", filename);
|
||||
LOG_INFO("Loaded %s", filename);
|
||||
state = LoadFileResult::LOAD_SUCCESS;
|
||||
}
|
||||
f.close();
|
||||
} else {
|
||||
LOG_ERROR("Could not open / read %s", filename);
|
||||
LOG_ERROR("Can't open/read %s", filename);
|
||||
}
|
||||
#else
|
||||
LOG_ERROR("ERROR: Filesystem not implemented");
|
||||
LOG_ERROR("Filesystem not implemented");
|
||||
state = LoadFileResult::NO_FILESYSTEM;
|
||||
#endif
|
||||
return state;
|
||||
@@ -2251,7 +2255,7 @@ void NodeDB::loadFromDisk()
|
||||
spiLock->lock();
|
||||
for (const char *filename : eventProfileFiles) {
|
||||
if (FSCom.exists(filename) && !FSCom.remove(filename))
|
||||
LOG_WARN("Unable to remove stale event profile file %s", filename);
|
||||
LOG_WARN("Can't remove stale event profile file %s", filename);
|
||||
}
|
||||
spiLock->unlock();
|
||||
#endif
|
||||
@@ -2269,7 +2273,7 @@ void NodeDB::loadFromDisk()
|
||||
const size_t usedBytes = fsUsedBytes();
|
||||
eventProfileStorageUnavailable = !hasEventProfileStorageSpace(totalBytes, usedBytes);
|
||||
if (eventProfileStorageUnavailable) {
|
||||
LOG_ERROR("Event profile requires %u bytes free; only %u bytes available. Profile changes will not persist.",
|
||||
LOG_ERROR("Event profile needs %u bytes free; only %u available. Changes won't persist",
|
||||
static_cast<unsigned>(EVENT_PROFILE_STORAGE_RESERVATION_BYTES),
|
||||
static_cast<unsigned>(totalBytes >= usedBytes ? totalBytes - usedBytes : 0));
|
||||
}
|
||||
@@ -2293,7 +2297,7 @@ void NodeDB::loadFromDisk()
|
||||
#if defined(FACTORY_INSTALL) && !defined(ARCH_PORTDUINO)
|
||||
spiLock->lock();
|
||||
if (!FSCom.exists("/prefs/" xstr(BUILD_EPOCH))) {
|
||||
LOG_WARN("Factory Install Reset!");
|
||||
LOG_WARN("Factory Install Reset");
|
||||
rmDir("/prefs");
|
||||
FSCom.mkdir("/prefs");
|
||||
File f2 = FSCom.open("/prefs/" xstr(BUILD_EPOCH), FILE_O_WRITE);
|
||||
@@ -2307,11 +2311,11 @@ void NodeDB::loadFromDisk()
|
||||
spiLock->lock();
|
||||
if (FSCom.exists(legacyPrefFileName)) {
|
||||
spiLock->unlock();
|
||||
LOG_WARN("Legacy prefs version found, factory resetting");
|
||||
LOG_WARN("Legacy prefs version, factory reset");
|
||||
if (loadProto(configFileName, meshtastic_LocalConfig_size, sizeof(meshtastic_LocalConfig), &meshtastic_LocalConfig_msg,
|
||||
&config) == LoadFileResult::LOAD_SUCCESS &&
|
||||
config.has_security && config.security.private_key.size > 0) {
|
||||
LOG_DEBUG("Saving backup of security config and keys");
|
||||
LOG_DEBUG("Backup security config and keys");
|
||||
backupSecurity = config.security;
|
||||
}
|
||||
spiLock->lock();
|
||||
@@ -2332,7 +2336,7 @@ void NodeDB::loadFromDisk()
|
||||
// Encrypted storage is locked. Install defaults and wait for the
|
||||
// passphrase over BLE/serial; PhoneAPI::handleLockdownAuthInline
|
||||
// calls reloadFromDisk() once the storage is unlocked.
|
||||
LOG_WARN("NodeDB: Encrypted storage locked, using default config until unlocked");
|
||||
LOG_WARN("NodeDB: Encrypted storage locked, default config until unlocked");
|
||||
installDefaultNodeDatabase();
|
||||
installDefaultDeviceState();
|
||||
installDefaultConfig();
|
||||
@@ -2443,7 +2447,7 @@ void NodeDB::loadFromDisk()
|
||||
// Attempt recovery of owner fields from our own NodeDB entry if available.
|
||||
const meshtastic_NodeInfoLite *us = getMeshNode(getNodeNum());
|
||||
if (nodeInfoLiteHasUser(us)) {
|
||||
LOG_WARN("Restoring owner fields (long_name/short_name/is_licensed/is_unmessagable) from NodeDB for our node 0x%08x",
|
||||
LOG_WARN("Restore owner fields (long_name/short_name/is_licensed/is_unmessagable) from NodeDB for node 0x%08x",
|
||||
us->num);
|
||||
// owner.long_name (40) is wider than the lite source (25); bound by the source
|
||||
memcpy(owner.long_name, us->long_name, sizeof(us->long_name));
|
||||
@@ -2458,7 +2462,7 @@ void NodeDB::loadFromDisk()
|
||||
saveToDisk(SEGMENT_DEVICESTATE);
|
||||
}
|
||||
} else {
|
||||
LOG_INFO("Loaded saved devicestate version %d", devicestate.version);
|
||||
LOG_INFO("Loaded saved devicestate v%d", devicestate.version);
|
||||
}
|
||||
|
||||
// Devicestate saved by firmware that allowed 39-byte names gets clamped on
|
||||
@@ -2484,7 +2488,7 @@ void NodeDB::loadFromDisk()
|
||||
config.lora = eventLora;
|
||||
state = LoadFileResult::LOAD_SUCCESS;
|
||||
initializedEventConfig = true;
|
||||
LOG_INFO("Initialized event config without modifying %s", STANDARD_CONFIG_FILE_NAME);
|
||||
LOG_INFO("Init event config without modifying %s", STANDARD_CONFIG_FILE_NAME);
|
||||
} else {
|
||||
// Keep the event load outcome because loadProto() clears config before decoding.
|
||||
// A normal decode failure must not create a replacement identity.
|
||||
@@ -2498,7 +2502,7 @@ void NodeDB::loadFromDisk()
|
||||
// our NodeNum (== crc32(public_key)) and orphan us on the mesh. configDecodeFailed freezes identity and
|
||||
// skips persisting (see ctor), so a transient failure self-heals on the next clean boot. A genuinely
|
||||
// absent config returns OTHER_FAILURE, so this never fires on first boot. Boot degraded + radio-silent.
|
||||
LOG_ERROR("Config decode failed - freezing identity, booting degraded (radio silent until restored)");
|
||||
LOG_ERROR("Config decode failed - freeze identity, boot degraded (radio silent until restored)");
|
||||
configDecodeFailed = true;
|
||||
installDefaultConfig(true);
|
||||
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
||||
@@ -2511,7 +2515,7 @@ void NodeDB::loadFromDisk()
|
||||
LOG_WARN("config %d is old, discard", config.version);
|
||||
installDefaultConfig(true);
|
||||
} else {
|
||||
LOG_INFO("Loaded saved config version %d", config.version);
|
||||
LOG_INFO("Loaded saved config v%d", config.version);
|
||||
}
|
||||
configLoadComplete = true;
|
||||
|
||||
@@ -2559,12 +2563,12 @@ void NodeDB::loadFromDisk()
|
||||
// This is the first durable event-profile write. A failed write is
|
||||
// safe: normal files remain untouched and the next event boot retries.
|
||||
if (!saveToDisk(SEGMENT_CONFIG))
|
||||
LOG_ERROR("Unable to persist initial event config");
|
||||
LOG_ERROR("Can't persist initial event config");
|
||||
}
|
||||
#endif
|
||||
|
||||
if (backupSecurity.private_key.size > 0) {
|
||||
LOG_DEBUG("Restoring backup of security config");
|
||||
LOG_DEBUG("Restore security config backup");
|
||||
config.security = backupSecurity;
|
||||
saveToDisk(SEGMENT_CONFIG);
|
||||
}
|
||||
@@ -2583,7 +2587,7 @@ void NodeDB::loadFromDisk()
|
||||
}
|
||||
if (sum == 0) {
|
||||
numAdminKeys += 1;
|
||||
LOG_INFO("Admin 0 key zero. Loading hard coded key from user preferences.");
|
||||
LOG_INFO("Admin 0 key zero. Load hard coded key from user prefs");
|
||||
memcpy(config.security.admin_key[0].bytes, userprefs_admin_key_0, 32);
|
||||
config.security.admin_key[0].size = 32;
|
||||
}
|
||||
@@ -2596,7 +2600,7 @@ void NodeDB::loadFromDisk()
|
||||
}
|
||||
if (sum == 0) {
|
||||
numAdminKeys += 1;
|
||||
LOG_INFO("Admin 1 key zero. Loading hard coded key from user preferences.");
|
||||
LOG_INFO("Admin 1 key zero. Load hard coded key from user prefs");
|
||||
memcpy(config.security.admin_key[1].bytes, userprefs_admin_key_1, 32);
|
||||
config.security.admin_key[1].size = 32;
|
||||
}
|
||||
@@ -2609,14 +2613,14 @@ void NodeDB::loadFromDisk()
|
||||
}
|
||||
if (sum == 0) {
|
||||
numAdminKeys += 1;
|
||||
LOG_INFO("Admin 2 key zero. Loading hard coded key from user preferences.");
|
||||
LOG_INFO("Admin 2 key zero. Load hard coded key from user prefs");
|
||||
memcpy(config.security.admin_key[2].bytes, userprefs_admin_key_2, 32);
|
||||
config.security.admin_key[2].size = 32;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (numAdminKeys > 0) {
|
||||
LOG_INFO("Saving %d hard coded admin keys.", numAdminKeys);
|
||||
LOG_INFO("Saving %d hard coded admin keys", numAdminKeys);
|
||||
config.security.admin_key_count = numAdminKeys;
|
||||
saveToDisk(SEGMENT_CONFIG);
|
||||
}
|
||||
@@ -2630,7 +2634,7 @@ void NodeDB::loadFromDisk()
|
||||
LOG_WARN("moduleConfig %d is old, discard", moduleConfig.version);
|
||||
installDefaultModuleConfig();
|
||||
} else {
|
||||
LOG_INFO("Loaded saved moduleConfig version %d", moduleConfig.version);
|
||||
LOG_INFO("Loaded saved moduleConfig v%d", moduleConfig.version);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2653,7 +2657,7 @@ void NodeDB::loadFromDisk()
|
||||
LOG_WARN("channelFile %d is old, discard", channelFile.version);
|
||||
installDefaultChannels();
|
||||
} else {
|
||||
LOG_INFO("Loaded saved channelFile version %d", channelFile.version);
|
||||
LOG_INFO("Loaded saved channelFile v%d", channelFile.version);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2705,7 +2709,7 @@ void NodeDB::loadFromDisk()
|
||||
if (activeBackupExists && !EncryptedStorage::isEncrypted(backupFileName)) {
|
||||
LOG_INFO("Migrating %s to encrypted storage", backupFileName);
|
||||
if (!EncryptedStorage::migrateFile(backupFileName)) {
|
||||
LOG_ERROR("Unable to migrate %s to encrypted storage", backupFileName);
|
||||
LOG_ERROR("Can't migrate %s to encrypted storage", backupFileName);
|
||||
storageCorruptThisLoad = true;
|
||||
}
|
||||
}
|
||||
@@ -2724,7 +2728,7 @@ void NodeDB::loadFromDisk()
|
||||
if (exists && !EncryptedStorage::isEncrypted(fn)) {
|
||||
LOG_INFO("Migrating inactive radio profile %s to encrypted storage", fn);
|
||||
if (!EncryptedStorage::migrateFile(fn)) {
|
||||
LOG_ERROR("Unable to migrate %s to encrypted storage", fn);
|
||||
LOG_ERROR("Can't migrate %s to encrypted storage", fn);
|
||||
storageCorruptThisLoad = true;
|
||||
}
|
||||
}
|
||||
@@ -2736,7 +2740,7 @@ void NodeDB::loadFromDisk()
|
||||
|
||||
// 2.4.X - configuration migration to update new default intervals
|
||||
if (moduleConfig.version < 23) {
|
||||
LOG_DEBUG("ModuleConfig version %d is stale, upgrading to new default intervals", moduleConfig.version);
|
||||
LOG_DEBUG("ModuleConfig v%d stale, upgrade to new default intervals", moduleConfig.version);
|
||||
moduleConfig.version = DEVICESTATE_CUR_VER;
|
||||
if (moduleConfig.telemetry.device_update_interval == 900)
|
||||
moduleConfig.telemetry.device_update_interval = 0;
|
||||
@@ -2830,7 +2834,7 @@ bool NodeDB::reloadFromDisk()
|
||||
loadFromDisk();
|
||||
|
||||
if (storageCorruptThisLoad) {
|
||||
LOG_ERROR("NodeDB: storage decrypt/decode failed during reload - surfacing as corrupt");
|
||||
LOG_ERROR("NodeDB: reload decrypt/decode failed - treat as corrupt");
|
||||
// Leave the radio sleeping. Caller will lock storage and emit
|
||||
// a LOCKED(storage_corrupt) status; we must not reconfigure
|
||||
// the chip with the locked-default placeholder values still
|
||||
@@ -2875,7 +2879,7 @@ bool NodeDB::disableLockdownToPlaintext()
|
||||
moduleConfigFileName, deviceStateFileName, nodeDatabaseFileName};
|
||||
for (const char *fn : filesToCheck) {
|
||||
if (!EncryptedStorage::migrateFileToPlaintext(fn)) {
|
||||
LOG_ERROR("NodeDB: failed to revert %s to plaintext; aborting disable (device stays in lockdown)", fn);
|
||||
LOG_ERROR("NodeDB: revert %s to plaintext failed; abort disable (stays in lockdown)", fn);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -2904,7 +2908,7 @@ bool NodeDB::saveProto(const char *filename, size_t protoSize, const pb_msgdesc_
|
||||
// do not try to save anything if power level is not safe. In many cases flash will be lock-protected
|
||||
// and all writes will fail anyway. Device should be sleeping at this point anyway.
|
||||
if (!powerHAL_isPowerLevelSafe()) {
|
||||
LOG_ERROR("Error: trying to saveProto() on unsafe device power level.");
|
||||
LOG_ERROR("saveProto() on unsafe device power level");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2926,7 +2930,7 @@ bool NodeDB::saveProto(const char *filename, size_t protoSize, const pb_msgdesc_
|
||||
|
||||
pb_ostream_t stream = pb_ostream_from_buffer(pbBuf.get(), protoSize);
|
||||
if (!pb_encode(&stream, fields, dest_struct)) {
|
||||
LOG_ERROR("Error: can't encode protobuf %s", PB_GET_ERROR(&stream));
|
||||
LOG_ERROR("Can't encode protobuf %s", PB_GET_ERROR(&stream));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2934,7 +2938,7 @@ bool NodeDB::saveProto(const char *filename, size_t protoSize, const pb_msgdesc_
|
||||
bool ok = EncryptedStorage::encryptAndWrite(filename, pbBuf.get(), encodedSize, fullAtomic);
|
||||
|
||||
if (!ok) {
|
||||
LOG_ERROR("EncryptedStorage: Failed to encrypt and write %s", filename);
|
||||
LOG_ERROR("EncryptedStorage: encrypt+write %s failed", filename);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
@@ -2948,7 +2952,7 @@ bool NodeDB::saveProto(const char *filename, size_t protoSize, const pb_msgdesc_
|
||||
pb_ostream_t stream = {&writecb, static_cast<Print *>(&f), protoSize};
|
||||
|
||||
if (!pb_encode(&stream, fields, dest_struct)) {
|
||||
LOG_ERROR("Error: can't encode protobuf %s", PB_GET_ERROR(&stream));
|
||||
LOG_ERROR("Can't encode protobuf %s", PB_GET_ERROR(&stream));
|
||||
} else {
|
||||
okay = true;
|
||||
}
|
||||
@@ -2956,10 +2960,10 @@ bool NodeDB::saveProto(const char *filename, size_t protoSize, const pb_msgdesc_
|
||||
bool writeSucceeded = f.close();
|
||||
|
||||
if (!okay || !writeSucceeded) {
|
||||
LOG_ERROR("Can't write prefs!");
|
||||
LOG_ERROR("Can't write prefs");
|
||||
}
|
||||
#else
|
||||
LOG_ERROR("ERROR: Filesystem not implemented");
|
||||
LOG_ERROR("Filesystem not implemented");
|
||||
#endif
|
||||
return okay;
|
||||
}
|
||||
@@ -2970,7 +2974,7 @@ bool NodeDB::saveChannelsToDisk()
|
||||
// do not try to save anything if power level is not safe. In many cases flash will be lock-protected
|
||||
// and all writes will fail anyway.
|
||||
if (!powerHAL_isPowerLevelSafe()) {
|
||||
LOG_ERROR("Error: trying to saveChannelsToDisk() on unsafe device power level.");
|
||||
LOG_ERROR("saveChannelsToDisk() on unsafe device power level");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2989,7 +2993,7 @@ bool NodeDB::saveDeviceStateToDisk()
|
||||
// do not try to save anything if power level is not safe. In many cases flash will be lock-protected
|
||||
// and all writes will fail anyway. Device should be sleeping at this point anyway.
|
||||
if (!powerHAL_isPowerLevelSafe()) {
|
||||
LOG_ERROR("Error: trying to saveDeviceStateToDisk() on unsafe device power level.");
|
||||
LOG_ERROR("saveDeviceStateToDisk() on unsafe device power level");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3017,7 +3021,7 @@ bool NodeDB::saveNodeDatabaseToDisk()
|
||||
// do not try to save anything if power level is not safe. In many cases flash will be lock-protected
|
||||
// and all writes will fail anyway. Device should be sleeping at this point anyway.
|
||||
if (!powerHAL_isPowerLevelSafe()) {
|
||||
LOG_ERROR("Error: trying to saveNodeDatabaseToDisk() on unsafe device power level.");
|
||||
LOG_ERROR("saveNodeDatabaseToDisk() on unsafe device power level");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3025,7 +3029,7 @@ bool NodeDB::saveNodeDatabaseToDisk()
|
||||
// would propagate through saveToDisk() and trigger fsFormat() mid-transfer.
|
||||
#ifdef FSCom
|
||||
if (xModem.isBusy()) {
|
||||
LOG_DEBUG("Deferring NodeDB save: xmodem transfer in progress");
|
||||
LOG_DEBUG("Defer NodeDB save: xmodem in progress");
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
@@ -3126,7 +3130,7 @@ bool NodeDB::saveToDiskNoRetry(int saveWhat)
|
||||
// do not try to save anything if power level is not safe. In many cases flash will be lock-protected
|
||||
// and all writes will fail anyway. Device should be sleeping at this point anyway.
|
||||
if (!powerHAL_isPowerLevelSafe()) {
|
||||
LOG_ERROR("Error: trying to saveToDiskNoRetry() on unsafe device power level.");
|
||||
LOG_ERROR("saveToDiskNoRetry() on unsafe device power level");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3156,11 +3160,11 @@ bool NodeDB::saveToDiskNoRetry(int saveWhat)
|
||||
#if USERPREFS_EVENT_MODE
|
||||
if (eventProfileStorageUnavailable) {
|
||||
if (saveWhat & SEGMENT_CONFIG) {
|
||||
LOG_WARN("Skipping event config write: insufficient profile storage at boot");
|
||||
LOG_WARN("Skip event config write: insufficient profile storage at boot");
|
||||
saveWhat &= ~SEGMENT_CONFIG;
|
||||
}
|
||||
if (saveWhat & SEGMENT_CHANNELS) {
|
||||
LOG_WARN("Skipping event channel write: insufficient profile storage at boot");
|
||||
LOG_WARN("Skip event channel write: insufficient profile storage at boot");
|
||||
saveWhat &= ~SEGMENT_CHANNELS;
|
||||
}
|
||||
}
|
||||
@@ -3224,14 +3228,14 @@ bool NodeDB::saveToDisk(int saveWhat)
|
||||
// do not try to save anything if power level is not safe. In many cases flash will be lock-protected
|
||||
// and all writes will fail anyway. Device should be sleeping at this point anyway.
|
||||
if (!powerHAL_isPowerLevelSafe()) {
|
||||
LOG_ERROR("Error: trying to saveToDisk() on unsafe device power level.");
|
||||
LOG_ERROR("saveToDisk() on unsafe device power level");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool success = saveToDiskNoRetry(saveWhat);
|
||||
|
||||
if (!success) {
|
||||
LOG_ERROR("Failed to save to disk, retrying");
|
||||
LOG_ERROR("Save to disk failed, retry");
|
||||
spiLock->lock();
|
||||
fsFormat();
|
||||
spiLock->unlock();
|
||||
@@ -3267,7 +3271,7 @@ uint32_t sinceLastSeen(const meshtastic_NodeInfoLite *n)
|
||||
|
||||
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
|
||||
// rx_time may be an uptime-seconds 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;
|
||||
@@ -3433,9 +3437,9 @@ void NodeDB::updateTelemetry(uint32_t nodeId, const meshtastic_Telemetry &t, RxS
|
||||
|
||||
if (t.which_variant == meshtastic_Telemetry_device_metrics_tag) {
|
||||
if (src == RX_SRC_LOCAL) {
|
||||
LOG_DEBUG("updateTelemetry LOCAL device");
|
||||
LOG_TRACE("updateTelemetry LOCAL device");
|
||||
} else {
|
||||
LOG_DEBUG("updateTelemetry REMOTE device node=0x%08x", nodeId);
|
||||
LOG_TRACE("updateTelemetry REMOTE device node=0x%08x", nodeId);
|
||||
}
|
||||
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
||||
concurrency::LockGuard guard(&satelliteMutex);
|
||||
@@ -3445,9 +3449,9 @@ void NodeDB::updateTelemetry(uint32_t nodeId, const meshtastic_Telemetry &t, RxS
|
||||
|
||||
} else if (t.which_variant == meshtastic_Telemetry_environment_metrics_tag) {
|
||||
if (src == RX_SRC_LOCAL) {
|
||||
LOG_DEBUG("updateTelemetry LOCAL env");
|
||||
LOG_TRACE("updateTelemetry LOCAL env");
|
||||
} else {
|
||||
LOG_DEBUG("updateTelemetry REMOTE env node=0x%08x", nodeId);
|
||||
LOG_TRACE("updateTelemetry REMOTE env node=0x%08x", nodeId);
|
||||
}
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
||||
concurrency::LockGuard guard(&satelliteMutex);
|
||||
@@ -3481,7 +3485,16 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact)
|
||||
}
|
||||
}
|
||||
info->num = contact.node_num;
|
||||
// CopyUserToNodeInfoLite assigns public_key unconditionally, and clients send add_contact before every
|
||||
// DM - often from an entry that carries no key at all. A contact may still supply or update a full
|
||||
// 32-byte key (that's what add_contact is for), but it must never *erase* a key we already hold, which
|
||||
// would be persisted below and break subsequent DMs with PKI_SEND_FAIL_PUBLIC_KEY.
|
||||
const meshtastic_NodeInfoLite_public_key_t storedKey = info->public_key;
|
||||
TypeConversions::CopyUserToNodeInfoLite(info, contact.user);
|
||||
if (storedKey.size == 32 && info->public_key.size != 32) {
|
||||
LOG_INFO("Contact 0x%08x has no key, keep the stored one", contact.node_num);
|
||||
info->public_key = storedKey;
|
||||
}
|
||||
if (contact.should_ignore) {
|
||||
// Block the contact and drop its rich satellite data, but keep the
|
||||
// public key copied above - an ignored peer keeps a usable identity
|
||||
@@ -3505,16 +3518,16 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact)
|
||||
if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) {
|
||||
// Special case for CLIENT_BASE: is_favorite has special meaning, and we don't want to automatically set it
|
||||
// without the user doing so deliberately. We don't normally expect users to use a CLIENT_BASE to send DMs or to add
|
||||
// contacts, but we should make sure it doesn't auto-favorite in case they do. Instead, as a workaround, we'll set
|
||||
// last_heard to now, so that the add_contact node doesn't immediately get evicted.
|
||||
info->last_heard = getTime();
|
||||
// contacts, but we should make sure it doesn't auto-favorite in case they do. Instead, as a workaround, we'll
|
||||
// stamp the contact as heard now, so that the add_contact node doesn't immediately get evicted.
|
||||
stampContactHeardNow(info);
|
||||
} else {
|
||||
// Normal case: set is_favorite to prevent expiration.
|
||||
// last_heard will remain as-is (or remain 0 if this entry wasn't in the nodeDB).
|
||||
// If the protected cap refuses the favorite, fall back to stamping last_heard so the
|
||||
// If the protected cap refuses the favorite, fall back to a heard-now stamp so the
|
||||
// contact still isn't the first eviction victim.
|
||||
if (!setProtectedFlag(info, NODEINFO_BITFIELD_IS_FAVORITE_MASK, true))
|
||||
info->last_heard = getTime();
|
||||
stampContactHeardNow(info);
|
||||
}
|
||||
|
||||
// As the clients will begin sending the contact with DMs, we want to strictly check if the node is manually verified
|
||||
@@ -3537,7 +3550,7 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde
|
||||
// Only a signed update may change the identity of a proven signer; our own record is exempt.
|
||||
// Checked before getOrCreateMeshNode so a refusal cannot evict; isKnownXeddsaSigner covers the warm tier.
|
||||
if (nodeId != getNodeNum() && isKnownXeddsaSigner(nodeId) && !xeddsaSigned) {
|
||||
LOG_WARN("Refusing unsigned identity update for node 0x%08x that previously signed", nodeId);
|
||||
LOG_WARN("Refuse unsigned identity update for 0x%08x that previously signed", nodeId);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3578,12 +3591,12 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde
|
||||
if (info->public_key.size == 32) { // if we have a key for this user already, don't overwrite with a new one
|
||||
// if the key doesn't match, don't update nodeDB at all.
|
||||
if (p.public_key.size != 32 || (memcmp(p.public_key.bytes, info->public_key.bytes, 32) != 0)) {
|
||||
LOG_WARN("Public Key mismatch, dropping NodeInfo");
|
||||
LOG_WARN("Public Key mismatch, drop NodeInfo");
|
||||
return false;
|
||||
}
|
||||
LOG_INFO("Public Key set for node, not updating!");
|
||||
LOG_INFO("Public Key set, not updating");
|
||||
} else if (p.public_key.size == 32) {
|
||||
LOG_INFO("Update Node Pubkey!");
|
||||
LOG_INFO("Update Node Pubkey");
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -3618,7 +3631,7 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde
|
||||
saveToDisk(SEGMENT_NODEDATABASE);
|
||||
lastNodeDbSave = millis();
|
||||
} else {
|
||||
LOG_DEBUG("Defer NodeDB saveToDisk for now");
|
||||
LOG_DEBUG("Defer NodeDB saveToDisk");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3648,7 +3661,7 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp)
|
||||
return;
|
||||
}
|
||||
if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.from) {
|
||||
LOG_DEBUG("Update DB node 0x%08x, rx_time=%u", mp.from, mp.rx_time);
|
||||
LOG_TRACE("Update DB node 0x%08x, rx_time=%u", mp.from, mp.rx_time);
|
||||
|
||||
// mp.from is unauthenticated, so rate-limit admission once the database is full: otherwise
|
||||
// invented node numbers churn it at packet rate and push real neighbours out.
|
||||
@@ -3667,9 +3680,13 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp)
|
||||
return;
|
||||
}
|
||||
|
||||
// Gate on has_rx_time, not truthiness - rx_time may hold a millis() placeholder.
|
||||
// Gate on has_rx_time, not truthiness - rx_time may hold an uptime-seconds placeholder.
|
||||
if (mp.has_rx_time)
|
||||
info->last_heard = mp.rx_time;
|
||||
else
|
||||
// rx_time is the arrival instant in uptime seconds. It goes to the RAM sidecar, not
|
||||
// last_heard, which only ever holds a real epoch or 0.
|
||||
recordHeardWhileClockUntrusted(getFrom(&mp), mp.rx_time);
|
||||
|
||||
// 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
|
||||
@@ -3852,7 +3869,7 @@ void NodeDB::sortMeshDB()
|
||||
}
|
||||
}
|
||||
}
|
||||
LOG_INFO("Sort took %u milliseconds", millis() - lastSort);
|
||||
LOG_INFO("Sort took %u ms", millis() - lastSort);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4085,6 +4102,84 @@ meshtastic_Config_DeviceConfig_Role NodeDB::getNodeRole(NodeNum n)
|
||||
return meshtastic_Config_DeviceConfig_Role_CLIENT;
|
||||
}
|
||||
|
||||
void NodeDB::recordHeardWhileClockUntrusted(NodeNum num, uint32_t heardAtUptime)
|
||||
{
|
||||
// Update in place if the node already has a stamp.
|
||||
for (auto &h : heardAt) {
|
||||
if (h.num == num) {
|
||||
h.heardAtUptimeSecs = heardAtUptime;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Otherwise take an empty slot, or reuse the oldest stamp.
|
||||
NodeHeardAt *victim = &heardAt[0];
|
||||
for (auto &h : heardAt) {
|
||||
if (h.num == 0) {
|
||||
victim = &h;
|
||||
break;
|
||||
}
|
||||
if (h.heardAtUptimeSecs < victim->heardAtUptimeSecs)
|
||||
victim = &h;
|
||||
}
|
||||
victim->num = num;
|
||||
victim->heardAtUptimeSecs = heardAtUptime;
|
||||
}
|
||||
|
||||
bool NodeDB::getHeardAtUptimeSecs(NodeNum num, uint32_t &stamp) const
|
||||
{
|
||||
for (const auto &h : heardAt) {
|
||||
if (h.num == num) {
|
||||
stamp = h.heardAtUptimeSecs;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
NodeDB::EvictionRecency NodeDB::evictionRecency(const meshtastic_NodeInfoLite *n) const
|
||||
{
|
||||
uint32_t stamp = 0;
|
||||
const bool heardThisBoot = getHeardAtUptimeSecs(n->num, stamp);
|
||||
return {heardThisBoot ? stamp : n->last_heard, heardThisBoot};
|
||||
}
|
||||
|
||||
bool NodeDB::evictionRecencyOlder(EvictionRecency candidate, EvictionRecency incumbent)
|
||||
{
|
||||
if (candidate.heardThisBoot != incumbent.heardThisBoot)
|
||||
return !candidate.heardThisBoot;
|
||||
return candidate.value < incumbent.value;
|
||||
}
|
||||
|
||||
void NodeDB::stampContactHeardNow(meshtastic_NodeInfoLite *info)
|
||||
{
|
||||
const uint32_t nowEpoch = getValidTime(RTCQualityFromNet);
|
||||
if (nowEpoch)
|
||||
info->last_heard = nowEpoch;
|
||||
else
|
||||
recordHeardWhileClockUntrusted(info->num, Time::getUptimeSecs());
|
||||
}
|
||||
|
||||
void NodeDB::backfillHeardAt()
|
||||
{
|
||||
const uint32_t nowEpoch = getValidTime(RTCQualityFromNet);
|
||||
if (nowEpoch == 0) // called before the clock was actually valid - nothing to date against
|
||||
return;
|
||||
const uint32_t nowUptimeSecs = Time::getUptimeSecs();
|
||||
for (auto &h : heardAt) {
|
||||
if (h.num == 0)
|
||||
continue;
|
||||
meshtastic_NodeInfoLite *info = getMeshNode(h.num);
|
||||
if (info) {
|
||||
// Both stamps are monotonic uptime seconds, so the elapsed term is exact at any age.
|
||||
// Never move last_heard backwards: the node may since have been re-heard on a good clock.
|
||||
const uint32_t elapsedSecs = nowUptimeSecs - h.heardAtUptimeSecs;
|
||||
if (elapsedSecs < nowEpoch && nowEpoch - elapsedSecs > info->last_heard)
|
||||
info->last_heard = nowEpoch - elapsedSecs;
|
||||
}
|
||||
h = {}; // evicted or converted either way, the stamp's job is done
|
||||
}
|
||||
}
|
||||
|
||||
/// Find a node in our DB, create an empty NodeInfo if missing
|
||||
meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
|
||||
{
|
||||
@@ -4092,11 +4187,12 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
|
||||
|
||||
if (!lite) {
|
||||
if (isFull()) {
|
||||
LOG_INFO("Node database full with %i nodes and %u bytes free. Erasing oldest entry", numMeshNodes,
|
||||
memGet.getFreeHeap());
|
||||
LOG_INFO("Node database full: %i nodes, %u bytes free. Erase oldest", numMeshNodes, memGet.getFreeHeap());
|
||||
// look for oldest node and erase it
|
||||
uint32_t oldest = UINT32_MAX;
|
||||
uint32_t oldestBoring = UINT32_MAX;
|
||||
// Newest-possible sentinel: a zeroed init ranks older than every candidate, so nothing
|
||||
// would ever be selected. Keep it maximal even though the index guards below also cover it.
|
||||
EvictionRecency oldest = {UINT32_MAX, true};
|
||||
EvictionRecency oldestBoring = {UINT32_MAX, true};
|
||||
int oldestIndex = -1;
|
||||
int oldestBoringIndex = -1;
|
||||
for (int i = 1; i < numMeshNodes; i++) {
|
||||
@@ -4104,14 +4200,19 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
|
||||
const bool isFavoriteNode = nodeInfoLiteIsFavorite(cand);
|
||||
const bool isIgnored = nodeInfoLiteIsIgnored(cand);
|
||||
const bool isVerified = nodeInfoLiteIsKeyManuallyVerified(cand);
|
||||
// last_heard, except that nodes heard this boot before the clock became trusted
|
||||
// rank by their RAM arrival stamp instead of the 0 in the stored field.
|
||||
const EvictionRecency candRecency = evictionRecency(cand);
|
||||
// Simply the oldest non-favorite, non-ignored, non-verified node
|
||||
if (!isFavoriteNode && !isIgnored && !isVerified && cand->last_heard < oldest) {
|
||||
oldest = cand->last_heard;
|
||||
if (!isFavoriteNode && !isIgnored && !isVerified &&
|
||||
(oldestIndex == -1 || evictionRecencyOlder(candRecency, oldest))) {
|
||||
oldest = candRecency;
|
||||
oldestIndex = i;
|
||||
}
|
||||
// The oldest "boring" node
|
||||
if (!isFavoriteNode && !isIgnored && cand->public_key.size == 0 && cand->last_heard < oldestBoring) {
|
||||
oldestBoring = cand->last_heard;
|
||||
if (!isFavoriteNode && !isIgnored && cand->public_key.size == 0 &&
|
||||
(oldestBoringIndex == -1 || evictionRecencyOlder(candRecency, oldestBoring))) {
|
||||
oldestBoring = candRecency;
|
||||
oldestBoringIndex = i;
|
||||
}
|
||||
}
|
||||
@@ -4188,7 +4289,7 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
LOG_INFO("Adding node to database with %i nodes and %u bytes free!", numMeshNodes, memGet.getFreeHeap());
|
||||
LOG_INFO("Add node to database: %i nodes, %u bytes free", numMeshNodes, memGet.getFreeHeap());
|
||||
}
|
||||
|
||||
return lite;
|
||||
@@ -4265,14 +4366,14 @@ bool NodeDB::generateCryptoKeyPair(const uint8_t *privateKey)
|
||||
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
|
||||
keygenSuccess = true;
|
||||
} else {
|
||||
LOG_ERROR("Failed to generate public key from provided private key");
|
||||
LOG_ERROR("Can't generate public key from private key");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Try to regenerate public key from existing private key if it's valid and not low entropy
|
||||
else if (config.security.private_key.size == 32 && !keyIsLowEntropy) {
|
||||
config.security.public_key.size = 32;
|
||||
LOG_DEBUG("Regenerate PKI public key from existing private key");
|
||||
LOG_DEBUG("Regenerate PKI public key from private key");
|
||||
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
|
||||
keygenSuccess = true;
|
||||
}
|
||||
@@ -4332,7 +4433,7 @@ bool NodeDB::createNewIdentity()
|
||||
// still streamed to clients, and made any DM/admin aimed at it fail forever with PKI_SEND_FAIL_PUBLIC_KEY.
|
||||
// removeNodeByNum() drops the lite entry, its satellite stores, and the warm-tier copy.
|
||||
if (getMeshNode(oldNodeNum) != NULL) {
|
||||
LOG_DEBUG("Old node num %u is now %u, removing stale identity", oldNodeNum, newNodeNum);
|
||||
LOG_DEBUG("Old node num 0x%08x now 0x%08x, remove stale identity", oldNodeNum, newNodeNum);
|
||||
removeNodeByNum(oldNodeNum);
|
||||
} else {
|
||||
// Lite entry already absent: drop any orphaned satellite-store entries directly.
|
||||
@@ -4378,7 +4479,7 @@ bool NodeDB::backupPreferences(meshtastic_AdminMessage_BackupLocation location)
|
||||
if (success) {
|
||||
LOG_INFO("Saved backup preferences");
|
||||
} else {
|
||||
LOG_ERROR("Failed to save backup preferences to file");
|
||||
LOG_ERROR("Save backup prefs to file failed");
|
||||
}
|
||||
} else if (location == meshtastic_AdminMessage_BackupLocation_SD) {
|
||||
// TODO: After more mainline SD card support
|
||||
@@ -4395,7 +4496,7 @@ bool NodeDB::restorePreferences(meshtastic_AdminMessage_BackupLocation location,
|
||||
spiLock->lock();
|
||||
if (!FSCom.exists(backupFileName)) {
|
||||
spiLock->unlock();
|
||||
LOG_WARN("Could not restore. No backup file found");
|
||||
LOG_WARN("Can't restore, no backup file");
|
||||
return false;
|
||||
} else {
|
||||
spiLock->unlock();
|
||||
@@ -4430,12 +4531,12 @@ bool NodeDB::restorePreferences(meshtastic_AdminMessage_BackupLocation location,
|
||||
|
||||
success = saveToDisk(restoreWhat);
|
||||
if (success) {
|
||||
LOG_INFO("Restored preferences from backup");
|
||||
LOG_INFO("Restored prefs from backup");
|
||||
} else {
|
||||
LOG_ERROR("Failed to save restored preferences to flash");
|
||||
LOG_ERROR("Save restored prefs to flash failed");
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR("Failed to restore preferences from backup file");
|
||||
LOG_ERROR("Restore prefs from backup failed");
|
||||
}
|
||||
} else if (location == meshtastic_AdminMessage_BackupLocation_SD) {
|
||||
// TODO: After more mainline SD card support
|
||||
@@ -4459,7 +4560,7 @@ void recordCriticalError(meshtastic_CriticalErrorCode code, uint32_t address, co
|
||||
|
||||
// Currently portuino is mostly used for simulation. Make sure the user notices something really bad happened
|
||||
#ifdef ARCH_PORTDUINO
|
||||
LOG_ERROR("A critical failure occurred");
|
||||
LOG_ERROR("Critical failure");
|
||||
// TODO: Determine if other critical errors should also cause an immediate exit
|
||||
if (code == meshtastic_CriticalErrorCode_FLASH_CORRUPTION_RECOVERABLE ||
|
||||
code == meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE)
|
||||
|
||||
@@ -248,6 +248,14 @@ enum LoadFileResult {
|
||||
|
||||
enum UserLicenseStatus { NotKnown, NotLicensed, Licensed };
|
||||
|
||||
// RAM-only arrival stamp (monotonic uptime secs) for nodes heard before the wall clock was trusted,
|
||||
// backfilled into last_heard as an epoch once it is. last_heard persists, so it cannot hold this.
|
||||
// Bounded, linear-scan, reuse-oldest, never persisted - dies with the boot, as does its timebase.
|
||||
struct NodeHeardAt {
|
||||
NodeNum num = 0; ///< node this stamp describes; 0 == empty slot
|
||||
uint32_t heardAtUptimeSecs = 0; ///< Time::getUptimeSecs() when last heard
|
||||
};
|
||||
|
||||
class NodeDB
|
||||
{
|
||||
// NodeNum provisionalNodeNum; // if we are trying to find a node num this is our current attempt
|
||||
@@ -308,6 +316,10 @@ class NodeDB
|
||||
|
||||
void addFromContact(const meshtastic_SharedContact);
|
||||
|
||||
/// On the clock-becoming-trusted transition (see RTC.cpp): convert every RAM arrival stamp into
|
||||
/// a real last_heard epoch, never backwards, then empty the table. updateFrom() takes over.
|
||||
void backfillHeardAt();
|
||||
|
||||
/** Update position info for this node based on received position data
|
||||
*/
|
||||
void updatePosition(uint32_t nodeId, const meshtastic_Position &p, RxSource src = RX_SRC_RADIO);
|
||||
@@ -638,6 +650,31 @@ class NodeDB
|
||||
uint32_t lastBackupAttempt = 0; // when we last tried a backup automatically or manually
|
||||
uint32_t lastSort = 0; // When last sorted the nodeDB
|
||||
|
||||
/// See NodeHeardAt. Caps how many distinct nodes can be dated once the clock arrives; a node
|
||||
/// pushed out by reuse-oldest just stays "last heard: unknown", the same as before this table.
|
||||
static constexpr size_t kMaxHeardAt = 32;
|
||||
NodeHeardAt heardAt[kMaxHeardAt] = {};
|
||||
|
||||
/// Stamp (or re-stamp) a node's RAM arrival record; used instead of writing a non-epoch into
|
||||
/// last_heard whenever the wall clock is untrusted.
|
||||
void recordHeardWhileClockUntrusted(NodeNum num, uint32_t heardAtUptimeSecs);
|
||||
|
||||
/// addFromContact's anti-eviction stamp: a real epoch when the clock is trusted, otherwise a
|
||||
/// RAM arrival stamp that evictionRecency() honours - never a boot-relative last_heard.
|
||||
void stampContactHeardNow(meshtastic_NodeInfoLite *info);
|
||||
|
||||
/// Read the node's RAM arrival stamp. The boolean carries presence because uptime second 0 is valid.
|
||||
bool getHeardAtUptimeSecs(NodeNum num, uint32_t &stamp) const;
|
||||
|
||||
struct EvictionRecency {
|
||||
uint32_t value;
|
||||
bool heardThisBoot;
|
||||
};
|
||||
|
||||
/// Eviction ranking with current-boot stamps newer than every persisted epoch.
|
||||
EvictionRecency evictionRecency(const meshtastic_NodeInfoLite *n) const;
|
||||
static bool evictionRecencyOlder(EvictionRecency candidate, EvictionRecency incumbent);
|
||||
|
||||
/*
|
||||
* Internal boolean to track sorting paused
|
||||
*/
|
||||
|
||||
+12
-13
@@ -64,13 +64,13 @@ bool PacketHistory::wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpd
|
||||
bool *wasUpgraded)
|
||||
{
|
||||
if (!initOk()) {
|
||||
LOG_ERROR("Packet History - Was Seen Recently: NOT INITIALIZED!");
|
||||
LOG_ERROR("Packet History - Was Seen Recently: NOT INITIALIZED");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (p->id == 0) {
|
||||
#if VERBOSE_PACKET_HISTORY
|
||||
LOG_DEBUG("Packet History - Was Seen Recently: ID is 0, not a floodable message");
|
||||
LOG_DEBUG("Packet History - Was Seen Recently: ID 0, not floodable");
|
||||
#endif
|
||||
return false; // Not a floodable message ID, so we don't care
|
||||
}
|
||||
@@ -107,8 +107,8 @@ bool PacketHistory::wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpd
|
||||
|
||||
// Check for hop_limit upgrade scenario
|
||||
if (seenRecently && wasUpgraded && getHighestHopLimit(*found) < p->hop_limit) {
|
||||
LOG_DEBUG("Packet History - Hop limit upgrade: packet 0x%08x from hop_limit=%d to hop_limit=%d", p->id,
|
||||
getHighestHopLimit(*found), p->hop_limit);
|
||||
LOG_TRACE("Packet History - Hop limit upgrade: packet 0x%08x hop_limit=%d -> %d", p->id, getHighestHopLimit(*found),
|
||||
p->hop_limit);
|
||||
*wasUpgraded = true;
|
||||
} else if (wasUpgraded) {
|
||||
*wasUpgraded = false; // Initialize to false if not an upgrade
|
||||
@@ -234,7 +234,7 @@ void PacketHistory::hashInsert(NodeNum sender, PacketId id, uint16_t slotIdx)
|
||||
}
|
||||
bucket = (bucket + 1) & hashMask;
|
||||
}
|
||||
LOG_ERROR("Packet History - hashInsert: table full or corrupted, rebuilding");
|
||||
LOG_ERROR("Packet History - hashInsert: table full or corrupt, rebuild");
|
||||
hashRebuild();
|
||||
}
|
||||
|
||||
@@ -357,8 +357,7 @@ void PacketHistory::insert(const PacketRecord &r)
|
||||
it = (base + recentPacketsCapacity);
|
||||
} else {
|
||||
if (it->rxTimeMsec == 0) {
|
||||
LOG_WARN("Packet History - insert: Found packet s=0x%08x id=0x%08x with rxTimeMsec = 0, slot %d/%d. Should never "
|
||||
"happen!",
|
||||
LOG_WARN("Packet History - insert: Found s=0x%08x id=0x%08x rxTimeMsec = 0, slot %d/%d. Should never happen",
|
||||
it->sender, it->id, it - base, recentPacketsCapacity);
|
||||
}
|
||||
if ((now_millis - it->rxTimeMsec) > OldtrxTimeMsec) { // 49.7 days rollover friendly
|
||||
@@ -373,7 +372,7 @@ void PacketHistory::insert(const PacketRecord &r)
|
||||
}
|
||||
|
||||
if (tu == NULL) {
|
||||
LOG_ERROR("Packet History - insert: No free slot, no matched packet, no oldest to reuse. Something leaked."); // mx
|
||||
LOG_ERROR("Packet History - insert: No free/matched/oldest slot. Something leaked"); // mx
|
||||
// assert(false); // This should never happen, we should always have at least one packet to clear
|
||||
return; // Return early if we can't update the history
|
||||
}
|
||||
@@ -399,7 +398,7 @@ void PacketHistory::insert(const PacketRecord &r)
|
||||
} else {
|
||||
// debug only
|
||||
#if VERBOSE_PACKET_HISTORY
|
||||
LOG_WARN("Packet History - insert: Reusing slot aged %.3fs < %ds with MATCHED PACKET - this is normal",
|
||||
LOG_WARN("Packet History - insert: Reusing slot aged %.3fs < %ds with MATCHED PACKET - normal",
|
||||
OldtrxTimeMsec / 1000., RECENT_WARN_AGE / 1000);
|
||||
#endif
|
||||
}
|
||||
@@ -424,7 +423,7 @@ void PacketHistory::insert(const PacketRecord &r)
|
||||
|
||||
if (r.rxTimeMsec == 0) {
|
||||
#if VERBOSE_PACKET_HISTORY
|
||||
LOG_WARN("Packet History - insert: I will not store packet with rxTimeMsec = 0.");
|
||||
LOG_WARN("Packet History - insert: Won't store packet with rxTimeMsec = 0");
|
||||
#endif
|
||||
return; // Return early if we can't update the history
|
||||
}
|
||||
@@ -457,7 +456,7 @@ void PacketHistory::insert(const PacketRecord &r)
|
||||
bool PacketHistory::wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender, bool *wasSole)
|
||||
{
|
||||
if (!initOk()) {
|
||||
LOG_ERROR("PacketHistory - wasRelayer: NOT INITIALIZED!");
|
||||
LOG_ERROR("PacketHistory - wasRelayer: NOT INITIALIZED");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -527,7 +526,7 @@ void PacketHistory::checkRelayers(uint8_t relayer1, uint8_t relayer2, uint32_t i
|
||||
*r2WasSole = false;
|
||||
|
||||
if (!initOk()) {
|
||||
LOG_ERROR("PacketHistory - checkRelayers: NOT INITIALIZED!");
|
||||
LOG_ERROR("PacketHistory - checkRelayers: NOT INITIALIZED");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -545,7 +544,7 @@ void PacketHistory::checkRelayers(uint8_t relayer1, uint8_t relayer2, uint32_t i
|
||||
void PacketHistory::removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender)
|
||||
{
|
||||
if (!initOk()) {
|
||||
LOG_ERROR("Packet History - remove Relayer: NOT INITIALIZED!");
|
||||
LOG_ERROR("Packet History - remove Relayer: NOT INITIALIZED");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+45
-25
@@ -216,11 +216,11 @@ static PhoneAuthSlot *findOrAllocSlot_LH(PhoneAPI *p)
|
||||
if (!s.authorized) {
|
||||
s.who = p;
|
||||
s.epoch = 0;
|
||||
LOG_WARN("Lockdown: auth slot table full, evicted stale unauthorized slot for new PhoneAPI %p", p);
|
||||
LOG_WARN("Lockdown: auth slots full, evicted stale unauthorized slot for new PhoneAPI %p", p);
|
||||
return &s;
|
||||
}
|
||||
}
|
||||
LOG_WARN("Lockdown: auth slot table full of authorized sessions, refusing new PhoneAPI %p (fail-closed)", p);
|
||||
LOG_WARN("Lockdown: auth slots full of authorized sessions, refuse new PhoneAPI %p (fail-closed)", p);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -304,7 +304,7 @@ void PhoneAPI::handleStartConfig()
|
||||
if (config_nonce == SPECIAL_NONCE_ONLY_NODES) {
|
||||
// If client only wants node info, jump directly to sending nodes
|
||||
state = STATE_SEND_OWN_NODEINFO;
|
||||
LOG_INFO("Client only wants node info, skipping other config");
|
||||
LOG_INFO("Client only wants node info, skip other config");
|
||||
} else {
|
||||
state = STATE_SEND_MY_INFO;
|
||||
}
|
||||
@@ -457,7 +457,7 @@ bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength)
|
||||
ourNum != 0 && toRadioScratch.packet.which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
|
||||
toRadioScratch.packet.decoded.portnum == meshtastic_PortNum_ADMIN_APP && toRadioScratch.packet.to == ourNum;
|
||||
if (!isLocalAdmin) {
|
||||
LOG_INFO("Lockdown: Dropping non-admin ToRadio packet from unauthorized client");
|
||||
LOG_INFO("Lockdown: Drop non-admin ToRadio packet from unauthorized client");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -475,7 +475,7 @@ bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength)
|
||||
case meshtastic_ToRadio_xmodemPacket_tag:
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
if (!getAdminAuthorized()) {
|
||||
LOG_INFO("Lockdown: Dropping xmodem packet from unauthorized client");
|
||||
LOG_INFO("Lockdown: Drop xmodem packet from unauthorized client");
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
@@ -486,17 +486,16 @@ bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength)
|
||||
break;
|
||||
#if !MESHTASTIC_EXCLUDE_MQTT
|
||||
case meshtastic_ToRadio_mqttClientProxyMessage_tag:
|
||||
LOG_DEBUG("Got MqttClientProxy message");
|
||||
LOG_TRACE("Got MqttClientProxy message");
|
||||
if (state != STATE_SEND_PACKETS) {
|
||||
LOG_WARN("Ignore MqttClientProxy message while completing config handshake");
|
||||
LOG_WARN("Ignore MqttClientProxy msg during config handshake");
|
||||
break;
|
||||
}
|
||||
if (mqtt && moduleConfig.mqtt.proxy_to_client_enabled && moduleConfig.mqtt.enabled &&
|
||||
(channels.anyMqttEnabled() || moduleConfig.mqtt.map_reporting_enabled)) {
|
||||
mqtt->onClientProxyReceive(toRadioScratch.mqttClientProxyMessage);
|
||||
} else {
|
||||
LOG_WARN("MqttClientProxy received but proxy is not enabled, no channels have up/downlink, or map reporting "
|
||||
"not enabled");
|
||||
LOG_WARN("MqttClientProxy received but proxy disabled, no up/downlink channels, or map reporting off");
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
@@ -511,11 +510,11 @@ bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength)
|
||||
// a queue-status reply.
|
||||
if (toRadioScratch.heartbeat.nonce == 1) {
|
||||
if (nodeInfoModule) {
|
||||
LOG_INFO("Broadcasting nodeinfo ping (serial)");
|
||||
LOG_INFO("Broadcast nodeinfo ping (serial)");
|
||||
nodeInfoModule->sendOurNodeInfo(NODENUM_BROADCAST, true, 0, true);
|
||||
}
|
||||
} else {
|
||||
LOG_DEBUG("Got client heartbeat");
|
||||
LOG_TRACE("Got client heartbeat");
|
||||
heartbeatReceived = true;
|
||||
}
|
||||
break;
|
||||
@@ -524,7 +523,7 @@ bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength)
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR("Error: ignore malformed toradio");
|
||||
LOG_ERROR("Ignore malformed toradio");
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -559,7 +558,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
fromRadioScratch.queueStatus = router->getQueueStatus();
|
||||
heartbeatReceived = false;
|
||||
size_t numbytes = pb_encode_to_bytes(buf, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratch);
|
||||
LOG_DEBUG("FromRadio=STATE_SEND_QUEUE_STATUS, numbytes=%u", numbytes);
|
||||
LOG_TRACE("FromRadio=STATE_SEND_QUEUE_STATUS, numbytes=%u", (unsigned)numbytes);
|
||||
return numbytes;
|
||||
}
|
||||
|
||||
@@ -572,7 +571,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
// Advance states as needed
|
||||
switch (state) {
|
||||
case STATE_SEND_NOTHING:
|
||||
LOG_DEBUG("FromRadio=STATE_SEND_NOTHING");
|
||||
LOG_TRACE("FromRadio=STATE_SEND_NOTHING");
|
||||
break;
|
||||
case STATE_SEND_MY_INFO:
|
||||
LOG_DEBUG("FromRadio=STATE_SEND_MY_INFO");
|
||||
@@ -616,6 +615,9 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
auto info = TypeConversions::ConvertToNodeInfo(us);
|
||||
info.has_hops_away = false;
|
||||
info.is_favorite = true;
|
||||
// NodeInfoLite dropped macaddr, so ConvertToUser() zero-fills it.
|
||||
if (info.has_user)
|
||||
memcpy(info.user.macaddr, owner.macaddr, sizeof(info.user.macaddr));
|
||||
{
|
||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||
nodeInfoForPhone = info;
|
||||
@@ -970,6 +972,14 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
}
|
||||
|
||||
if (infoToSend.num != 0) {
|
||||
// A record prefetched before the clock became trusted carries last_heard == 0 even
|
||||
// once the store is backfilled, so re-read it at send time: handshake ordering
|
||||
// (time-set vs node-list download) must not decide what the phone sees.
|
||||
if (infoToSend.last_heard == 0 && infoToSend.num != nodeDB->getNodeNum()) {
|
||||
const meshtastic_NodeInfoLite *fresh = nodeDB->getMeshNode(infoToSend.num);
|
||||
if (fresh)
|
||||
infoToSend.last_heard = fresh->last_heard;
|
||||
}
|
||||
// Just in case we stored a different user.id in the past, but should never happen going forward
|
||||
sprintf(infoToSend.user.id, "!%08x", infoToSend.num);
|
||||
|
||||
@@ -1000,7 +1010,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
} else {
|
||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_fileInfo_tag;
|
||||
fromRadioScratch.fileInfo = filesManifest.at(config_state);
|
||||
LOG_DEBUG("File: %s (%d) bytes", fromRadioScratch.fileInfo.file_name, fromRadioScratch.fileInfo.size_bytes);
|
||||
LOG_TRACE("File: %s (%d) bytes", fromRadioScratch.fileInfo.file_name, fromRadioScratch.fileInfo.size_bytes);
|
||||
config_state++;
|
||||
}
|
||||
break;
|
||||
@@ -1013,7 +1023,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
case STATE_SEND_PACKETS:
|
||||
pauseBluetoothLogging = false;
|
||||
// Do we have a message from the mesh or packet from the local device?
|
||||
LOG_DEBUG("FromRadio=STATE_SEND_PACKETS");
|
||||
LOG_TRACE("FromRadio=STATE_SEND_PACKETS");
|
||||
if (queueStatusPacketForPhone) {
|
||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_queueStatus_tag;
|
||||
fromRadioScratch.queueStatus = *queueStatusPacketForPhone;
|
||||
@@ -1098,7 +1108,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
return numbytes;
|
||||
}
|
||||
|
||||
LOG_DEBUG("No FromRadio packet available");
|
||||
LOG_TRACE("No FromRadio packet available");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1202,7 +1212,7 @@ void PhoneAPI::prefetchNodeInfos()
|
||||
nodeInfoQueue.push_back(info);
|
||||
// Log progress here (at fetch time) so readIndex is accurate and each value logs only once.
|
||||
if (readIndex == 2 || readIndex % 20 == 0) {
|
||||
LOG_DEBUG("nodeinfo: %d/%d", readIndex, nodeDB->getNumMeshNodes());
|
||||
LOG_TRACE("nodeinfo: %d/%d", readIndex, nodeDB->getNumMeshNodes());
|
||||
}
|
||||
added = true;
|
||||
}
|
||||
@@ -1804,7 +1814,7 @@ bool PhoneAPI::handleToRadioPacket(meshtastic_MeshPacket &p)
|
||||
return true;
|
||||
}
|
||||
case LocalAdminGate::DropUnauthorized:
|
||||
LOG_WARN("Lockdown: dropping admin payload variant=%d from unauthorized connection", admin.which_payload_variant);
|
||||
LOG_WARN("Lockdown: drop admin payload variant=%d from unauthorized connection", admin.which_payload_variant);
|
||||
return false;
|
||||
case LocalAdminGate::NotAdmin:
|
||||
case LocalAdminGate::AuthorizedPassThrough:
|
||||
@@ -1813,12 +1823,22 @@ bool PhoneAPI::handleToRadioPacket(meshtastic_MeshPacket &p)
|
||||
}
|
||||
#endif
|
||||
|
||||
// Reject before recording duplicate or per-port cooldown state, so a blocked
|
||||
// attempt cannot throttle a valid private-channel position retry.
|
||||
if (isBlockedEventCoordinatePacket(&p)) {
|
||||
LOG_DEBUG("Suppress phone coordinate send on event (everyone) channel");
|
||||
meshtastic_QueueStatus qs = router->getQueueStatus();
|
||||
service->sendQueueStatusToPhone(qs, 0, p.id);
|
||||
sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "Location sharing is disabled on this channel");
|
||||
return false;
|
||||
}
|
||||
|
||||
#if defined(ARCH_PORTDUINO)
|
||||
// For use with the simulator, we should not ignore duplicate packets from the phone
|
||||
if (SimRadio::instance == nullptr)
|
||||
#endif
|
||||
if (p.id > 0 && wasSeenRecently(p.id)) {
|
||||
LOG_DEBUG("Ignore packet from phone, already seen recently");
|
||||
LOG_DEBUG("Ignore phone packet, seen recently");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1878,7 +1898,7 @@ int PhoneAPI::onNotify(uint32_t newValue)
|
||||
// doesn't call this from idle)
|
||||
|
||||
if (state == STATE_SEND_PACKETS) {
|
||||
LOG_INFO("Tell client we have new packets %u", newValue);
|
||||
LOG_INFO("Tell client new packets %u", newValue);
|
||||
onNowHasData(newValue);
|
||||
} else {
|
||||
LOG_DEBUG("Client not yet interested in packets (state=%d)", state);
|
||||
@@ -2049,7 +2069,7 @@ bool PhoneAPI::handleLockdownAuthInline(const meshtastic_LockdownAuth &la)
|
||||
zeroPassphrase();
|
||||
return true;
|
||||
}
|
||||
LOG_INFO("Lockdown: LOCK NOW command received from authorized connection");
|
||||
LOG_INFO("Lockdown: LOCK NOW from authorized connection");
|
||||
EncryptedStorage::lockNow();
|
||||
revokeAllAuth();
|
||||
queueLockdownStatus(meshtastic_LockdownStatus_State_LOCKED, "", 0, 0, 0);
|
||||
@@ -2073,7 +2093,7 @@ bool PhoneAPI::handleLockdownAuthInline(const meshtastic_LockdownAuth &la)
|
||||
}
|
||||
if (!EncryptedStorage::isLockdownActive()) {
|
||||
// Already off - nothing to do; report DISABLED so the client UI settles.
|
||||
LOG_INFO("Lockdown: disable requested but lockdown is not active");
|
||||
LOG_INFO("Lockdown: disable requested but not active");
|
||||
queueLockdownStatus(meshtastic_LockdownStatus_State_DISABLED, "", 0, 0, 0);
|
||||
zeroPassphrase();
|
||||
return true;
|
||||
@@ -2156,7 +2176,7 @@ bool PhoneAPI::handleLockdownAuthInline(const meshtastic_LockdownAuth &la)
|
||||
slot->pendingUnlockAfterReload = true;
|
||||
}
|
||||
lockdownReloadPending = true;
|
||||
LOG_INFO("Lockdown: storage unlocked, awaiting reload before client visibility");
|
||||
LOG_INFO("Lockdown: storage unlocked, await reload before client visibility");
|
||||
}
|
||||
} else {
|
||||
LOG_INFO("Lockdown: passphrase re-verify for admin authorization");
|
||||
@@ -2166,7 +2186,7 @@ bool PhoneAPI::handleLockdownAuthInline(const meshtastic_LockdownAuth &la)
|
||||
// Storage was already unlocked - no reload needed. Authorize
|
||||
// and surface UNLOCKED to the client immediately.
|
||||
setAdminAuthorized(true);
|
||||
LOG_INFO("Lockdown: passphrase verified, this connection authorized");
|
||||
LOG_INFO("Lockdown: passphrase verified, connection authorized");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ uint32_t getPositionPrecisionForChannel(const meshtastic_Channel &channel)
|
||||
|
||||
uint32_t getPositionPrecisionForChannel(uint8_t channelIndex)
|
||||
{
|
||||
// Event-channel privacy takes precedence over every stored precision and key policy.
|
||||
if (channels.isEventChannel(channelIndex))
|
||||
return 0;
|
||||
|
||||
const meshtastic_Channel &ch = channels.getByIndex(channelIndex);
|
||||
if (ch.role == meshtastic_Channel_Role_DISABLED)
|
||||
return 0;
|
||||
|
||||
@@ -94,7 +94,7 @@ template <class T> class ProtobufModule : protected SinglePortModule
|
||||
LOG_INFO("Received %s from=0x%08x, id=0x%08x, portnum=%d, payloadlen=%d", name, mp.from, mp.id, p.portnum,
|
||||
p.payload.size);
|
||||
} else {
|
||||
LOG_ERROR("Error decoding proto module!");
|
||||
LOG_ERROR("Error decoding proto module");
|
||||
// if we can't decode it, nobody can process it!
|
||||
return ProcessMessage::STOP;
|
||||
}
|
||||
@@ -115,7 +115,7 @@ template <class T> class ProtobufModule : protected SinglePortModule
|
||||
if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, fields, &scratch)) {
|
||||
decoded = &scratch;
|
||||
} else {
|
||||
LOG_ERROR("Error decoding proto module!");
|
||||
LOG_ERROR("Error decoding proto module");
|
||||
// if we can't decode it, nobody can process it!
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ bool RF95Interface::init()
|
||||
return res == RADIOLIB_ERR_NONE;
|
||||
}
|
||||
|
||||
void RF95Interface::disableInterrupt()
|
||||
void RF95Interface::clearRadioIsr()
|
||||
{
|
||||
lora->clearDio0Action();
|
||||
}
|
||||
@@ -318,14 +318,14 @@ bool RF95Interface::isChannelActive()
|
||||
result = lora->scanChannel();
|
||||
|
||||
if (result == RADIOLIB_PREAMBLE_DETECTED) {
|
||||
// LOG_DEBUG("Channel is busy!");
|
||||
// LOG_DEBUG("Channel is busy");
|
||||
return true;
|
||||
}
|
||||
if (result != RADIOLIB_CHANNEL_FREE)
|
||||
LOG_ERROR("RF95 isChannelActive %s%d", radioLibErr, result);
|
||||
assert(result != RADIOLIB_ERR_WRONG_MODEM);
|
||||
|
||||
// LOG_DEBUG("Channel is free!");
|
||||
// LOG_DEBUG("Channel is free");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,14 +35,14 @@ class RF95Interface : public RadioLibInterface
|
||||
/**
|
||||
* Glue functions called from ISR land
|
||||
*/
|
||||
virtual void disableInterrupt() override;
|
||||
virtual void clearRadioIsr() override;
|
||||
|
||||
int16_t getCurrentRSSI() override;
|
||||
|
||||
/**
|
||||
* Enable a particular ISR callback glue function
|
||||
*/
|
||||
virtual void enableInterrupt(void (*callback)()) { lora->setDio0Action(callback, RISING); }
|
||||
virtual void setRadioIsr(void (*callback)()) override { lora->setDio0Action(callback, RISING); }
|
||||
|
||||
/** can we detect a LoRa preamble on the current channel? */
|
||||
virtual bool isChannelActive() override;
|
||||
|
||||
+10
-11
@@ -692,7 +692,7 @@ void getRegionPresetMap(meshtastic_LoRaRegionPresetMap &map)
|
||||
// log once and stop. An incomplete map means clients won't constrain the
|
||||
// omitted regions, so this must be discoverable rather than silent.
|
||||
if (map.region_groups_count >= maxRegions) {
|
||||
LOG_ERROR("Region preset map full at %u regions; remaining regions omitted", (unsigned)maxRegions);
|
||||
LOG_ERROR("Region preset map full at %u regions; rest omitted", (unsigned)maxRegions);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -832,11 +832,11 @@ uint32_t RadioInterface::getTxDelayMsecWeighted(meshtastic_MeshPacket *p)
|
||||
// LOG_DEBUG("rx_snr of %f so setting CWsize to:%d", snr, CWsize);
|
||||
if (shouldRebroadcastEarlyLikeRouter(p)) {
|
||||
delay = random(0, 2 * CWsize) * slotTimeMsec;
|
||||
LOG_DEBUG("rx_snr found in packet. Router: setting tx delay:%d", delay);
|
||||
LOG_DEBUG("rx_snr in packet. Router: tx delay:%d", delay);
|
||||
} else {
|
||||
// offset the maximum delay for routers: (2 * CWmax * slotTimeMsec)
|
||||
delay = (2 * CWmax * slotTimeMsec) + random(0, pow_of_2(CWsize)) * slotTimeMsec;
|
||||
LOG_DEBUG("rx_snr found in packet. Setting tx delay:%d", delay);
|
||||
LOG_DEBUG("rx_snr in packet. Tx delay:%d", delay);
|
||||
}
|
||||
|
||||
return delay;
|
||||
@@ -1108,7 +1108,7 @@ bool RadioInterface::checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraCo
|
||||
// Validation must still fail so callers route into the clamp, but quietly:
|
||||
// the clamp will accept this config by swapping regions, so don't record a
|
||||
// critical error or alarm the user over a change that is about to succeed.
|
||||
LOG_INFO("Preset %s implies region swap %s to %s, deferring to clamp", presetName, newRegion->name,
|
||||
LOG_INFO("Preset %s implies region swap %s to %s, defer to clamp", presetName, newRegion->name,
|
||||
swapRegion->name);
|
||||
return false;
|
||||
}
|
||||
@@ -1263,7 +1263,7 @@ void RadioInterface::applyModemConfig()
|
||||
// If custom CR is being used already, check if the new preset is higher
|
||||
if (loraConfig.coding_rate >= 5 && loraConfig.coding_rate <= 8 && loraConfig.coding_rate < newcr) {
|
||||
cr = newcr;
|
||||
LOG_INFO("Default Coding Rate is higher than custom setting, using %u", cr);
|
||||
LOG_INFO("Default Coding Rate above custom setting, use %u", cr);
|
||||
}
|
||||
// If the custom CR is higher than the preset, use it
|
||||
else if (loraConfig.coding_rate >= 5 && loraConfig.coding_rate <= 8 && loraConfig.coding_rate > newcr) {
|
||||
@@ -1276,8 +1276,7 @@ void RadioInterface::applyModemConfig()
|
||||
} else { // if not using preset, then just use the custom settings
|
||||
if (validateConfigLora(loraConfig)) {
|
||||
} else {
|
||||
LOG_WARN("Invalid LoRa config settings, cannot apply requested modem config - falling back to %s defaults",
|
||||
newRegion->name);
|
||||
LOG_WARN("Invalid LoRa config, can't apply modem config - fall back to %s defaults", newRegion->name);
|
||||
clampConfigLora(loraConfig);
|
||||
}
|
||||
// Clamp at the source so numFreqSlots below can never be 0 (a bandwidth-0 config may already be persisted)
|
||||
@@ -1370,9 +1369,9 @@ void RadioInterface::applyModemConfig()
|
||||
newRegion->freqEnd - newRegion->freqStart);
|
||||
LOG_INFO("numFreqSlots: %u x %.3fkHz", numFreqSlots, bw);
|
||||
if (newRegion->overrideSlot > 0) {
|
||||
LOG_INFO("Using region explicit override slot: %d", newRegion->overrideSlot);
|
||||
LOG_INFO("Region explicit override slot: %d", newRegion->overrideSlot);
|
||||
} else if (newRegion->overrideSlot == OVERRIDE_SLOT_PRESET_HASH) {
|
||||
LOG_INFO("Using region preset name hash for slot selection");
|
||||
LOG_INFO("Use region preset name hash for slot");
|
||||
}
|
||||
LOG_INFO("channel_num: %d", channel_num + 1);
|
||||
LOG_INFO("frequency: %f", getFreq());
|
||||
@@ -1410,7 +1409,7 @@ void RadioInterface::limitPower(int8_t loraMaxPower)
|
||||
maxPower = myRegion->powerLimit;
|
||||
|
||||
if ((power > maxPower) && !devicestate.owner.is_licensed) {
|
||||
LOG_INFO("Lower transmit power because of regulatory limits");
|
||||
LOG_INFO("Lower Tx power: regulatory limits");
|
||||
power = maxPower;
|
||||
}
|
||||
|
||||
@@ -1477,7 +1476,7 @@ size_t RadioInterface::beginSending(meshtastic_MeshPacket *p)
|
||||
radioBuffer.header.next_hop = p->next_hop;
|
||||
radioBuffer.header.relay_node = p->relay_node;
|
||||
if (p->hop_limit > HOP_MAX) {
|
||||
LOG_WARN("hop limit %d is too high, setting to %d", p->hop_limit, HOP_RELIABLE);
|
||||
LOG_WARN("hop limit %d too high, set to %d", p->hop_limit, HOP_RELIABLE);
|
||||
p->hop_limit = HOP_RELIABLE;
|
||||
}
|
||||
radioBuffer.header.flags =
|
||||
|
||||
@@ -107,7 +107,7 @@ bool RadioLibInterface::canSendImmediately()
|
||||
// If we've been trying to send the same packet more than one minute and we haven't gotten a
|
||||
// TX IRQ from the radio, the radio is probably broken.
|
||||
if (busyTx && !Throttle::isWithinTimespanMs(lastTxStart, 60000)) {
|
||||
LOG_ERROR("Hardware Failure! busyTx for more than 60s");
|
||||
LOG_ERROR("Hardware Failure! busyTx >60s");
|
||||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_TRANSMIT_FAILED);
|
||||
// reboot in 5 seconds when this condition occurs.
|
||||
rebootAtMsec = lastTxStart + 65000;
|
||||
@@ -131,14 +131,14 @@ bool RadioLibInterface::receiveDetected(uint16_t irq, unsigned long syncWordHead
|
||||
if (!(irq & syncWordHeaderValidFlag)) {
|
||||
// The HEADER_VALID flag should be set by now if it was really a packet, so ignore PREAMBLE_DETECTED flag
|
||||
activeReceiveStart = 0;
|
||||
LOG_DEBUG("Ignore false preamble detection");
|
||||
LOG_TRACE("Ignore false preamble detection");
|
||||
return false;
|
||||
} else {
|
||||
uint32_t maxPacketTimeMsec = getPacketTime(meshtastic_Constants_DATA_PAYLOAD_LEN + sizeof(PacketHeader));
|
||||
if (!Throttle::isWithinTimespanMs(activeReceiveStart, maxPacketTimeMsec)) {
|
||||
// We should have gotten an RX_DONE IRQ by now if it was really a packet, so ignore HEADER_VALID flag
|
||||
activeReceiveStart = 0;
|
||||
LOG_DEBUG("Ignore false header detection");
|
||||
LOG_TRACE("Ignore false header detection");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -187,7 +187,7 @@ ErrorCode RadioLibInterface::send(meshtastic_MeshPacket *p)
|
||||
#ifndef LORA_DISABLE_SENDING
|
||||
printPacket("enqueue for send", p);
|
||||
|
||||
LOG_DEBUG("txGood=%d,txRelay=%d,rxGood=%d,rxBad=%d", txGood, txRelay, rxGood, rxBad);
|
||||
LOG_TRACE("txGood=%d,txRelay=%d,rxGood=%d,rxBad=%d", txGood, txRelay, rxGood, rxBad);
|
||||
bool dropped = false;
|
||||
ErrorCode res = txQueue.enqueue(p, &dropped) ? ERRNO_OK : ERRNO_UNKNOWN;
|
||||
|
||||
@@ -290,7 +290,7 @@ void RadioLibInterface::updateNoiseFloor()
|
||||
|
||||
currentNoiseFloor = getAverageNoiseFloorInternal();
|
||||
|
||||
LOG_DEBUG("Noise floor: %d dBm (samples: %d, latest: %d dBm)", currentNoiseFloor, getNoiseFloorSampleCountInternal(), rssi);
|
||||
LOG_TRACE("Noise floor: %d dBm (samples: %d, latest: %d dBm)", currentNoiseFloor, getNoiseFloorSampleCountInternal(), rssi);
|
||||
}
|
||||
|
||||
uint8_t RadioLibInterface::getNoiseFloorSampleCountInternal() const
|
||||
@@ -339,7 +339,7 @@ void RadioLibInterface::resetNoiseFloor()
|
||||
currentSampleIndex = 0;
|
||||
isNoiseFloorBufferFull = false;
|
||||
currentNoiseFloor = NOISE_FLOOR_DEFAULT;
|
||||
LOG_INFO("Noise floor reset - rolling window collection will restart");
|
||||
LOG_INFO("Noise floor reset - rolling window will restart");
|
||||
}
|
||||
|
||||
bool RadioLibInterface::randomBytes(uint8_t *buffer, size_t length)
|
||||
@@ -445,7 +445,7 @@ void RadioLibInterface::onNotify(uint32_t notification)
|
||||
// The beacon's target radio config is invalid (bad preset/region, or an
|
||||
// unlicensed node keying up on a ham-only region). Drop the packet - never
|
||||
// transmit it on the current (home) config - and move on to the next queued packet.
|
||||
LOG_DEBUG("Beacon: invalid TX radio config, dropping packet 0x%08x", txp->id);
|
||||
LOG_DEBUG("Beacon: invalid TX radio config, drop packet 0x%08x", txp->id);
|
||||
meshtastic_MeshPacket *bad = txQueue.dequeue();
|
||||
MeshBeaconModule::clearTargetRadioSettings(bad);
|
||||
packetPool.release(bad);
|
||||
@@ -468,7 +468,7 @@ void RadioLibInterface::onNotify(uint32_t notification)
|
||||
txp = txQueue.dequeue();
|
||||
assert(txp);
|
||||
startSend(txp);
|
||||
LOG_DEBUG("%d packets remain in the TX queue", txQueue.getMaxLen() - txQueue.getFree());
|
||||
LOG_TRACE("%d packets in TX queue", txQueue.getMaxLen() - txQueue.getFree());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -505,7 +505,7 @@ void RadioLibInterface::setTransmitDelay()
|
||||
startTransmitTimer(true);
|
||||
} else {
|
||||
// If there is a SNR, start a timer scaled based on that SNR.
|
||||
LOG_DEBUG("rx_snr found. hop_limit:%d rx_snr:%f", p->hop_limit, p->rx_snr);
|
||||
LOG_TRACE("rx_snr found. hop_limit:%d rx_snr:%f", p->hop_limit, p->rx_snr);
|
||||
startTransmitTimerRebroadcast(p);
|
||||
}
|
||||
}
|
||||
@@ -539,7 +539,7 @@ void RadioLibInterface::clampToLateRebroadcastWindow(NodeNum from, PacketId id)
|
||||
p->tx_after = millis() + getTxDelayMsecWeightedWorst(p->rx_snr);
|
||||
bool dropped = false;
|
||||
if (txQueue.enqueue(p, &dropped)) {
|
||||
LOG_DEBUG("Move existing queued packet to the late rebroadcast window %dms from now", p->tx_after - millis());
|
||||
LOG_TRACE("Move queued packet to late rebroadcast window %ums from now", (uint32_t)(p->tx_after - millis()));
|
||||
} else {
|
||||
packetPool.release(p);
|
||||
}
|
||||
@@ -557,7 +557,7 @@ bool RadioLibInterface::removePendingTXPacket(NodeNum from, PacketId id, uint32_
|
||||
{
|
||||
meshtastic_MeshPacket *p = txQueue.remove(from, id, true, true, hop_limit_lt);
|
||||
if (p) {
|
||||
LOG_DEBUG("Dropping pending-TX packet 0x%08x with hop limit %d", p->id, p->hop_limit);
|
||||
LOG_DEBUG("Drop pending-TX packet 0x%08x, hop limit %d", p->id, p->hop_limit);
|
||||
packetPool.release(p);
|
||||
return true;
|
||||
}
|
||||
@@ -607,7 +607,7 @@ void RadioLibInterface::handleReceiveInterrupt()
|
||||
// when this is called, we should be in receive mode - if we are not, just jump out instead of bombing. Possible Race
|
||||
// Condition?
|
||||
if (!isReceiving) {
|
||||
LOG_ERROR("handleReceiveInterrupt called when not in rx mode, which shouldn't happen");
|
||||
LOG_ERROR("handleReceiveInterrupt called while not in rx mode");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -616,6 +616,13 @@ void RadioLibInterface::handleReceiveInterrupt()
|
||||
// read the number of actually received bytes
|
||||
size_t length = iface->getPacketLength();
|
||||
|
||||
// Some drivers report this as a 16 bit value, so a bad readback can overrun radioBuffer in readData()
|
||||
if (length > sizeof(radioBuffer)) {
|
||||
LOG_ERROR("Ignore rx packet, bad length %u", (unsigned int)length);
|
||||
rxBad++;
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t rxMsec = getPacketTime(length, true);
|
||||
|
||||
#ifndef DISABLE_WELCOME_UNSET
|
||||
@@ -634,7 +641,7 @@ void RadioLibInterface::handleReceiveInterrupt()
|
||||
#endif
|
||||
if (state != RADIOLIB_ERR_NONE) {
|
||||
// Log PacketHeader similar to RadioInterface::printPacket so we can try to match RX errors to other packets in the logs.
|
||||
LOG_ERROR("Ignore received packet due to error=%d (maybe id=0x%08x fr=0x%08x to=0x%08x flags=0x%02x rxSNR=%g rxRSSI=%i "
|
||||
LOG_ERROR("Ignore rx packet, error=%d (maybe id=0x%08x fr=0x%08x to=0x%08x flags=0x%02x rxSNR=%g rxRSSI=%i "
|
||||
"nextHop=0x%x relay=0x%x)",
|
||||
state, radioBuffer.header.id, radioBuffer.header.from, radioBuffer.header.to, radioBuffer.header.flags,
|
||||
iface->getSNR(), lround(iface->getRSSI()), radioBuffer.header.next_hop, radioBuffer.header.relay_node);
|
||||
@@ -759,7 +766,7 @@ bool RadioLibInterface::startSend(meshtastic_MeshPacket *txp)
|
||||
/* NOTE: Minimize the actions before startTransmit() to keep the time between
|
||||
channel scan and actual transmit as low as possible to avoid collisions. */
|
||||
if (disabled || !config.lora.tx_enabled) {
|
||||
LOG_WARN("Drop Tx packet because LoRa Tx disabled");
|
||||
LOG_WARN("Drop Tx packet: LoRa Tx disabled");
|
||||
#if !MESHTASTIC_EXCLUDE_BEACON
|
||||
// This packet may have already triggered a beacon radio switch in TRANSMIT_DELAY_COMPLETED;
|
||||
// since it never reaches completeSending() here, restore the radio so it isn't left on the
|
||||
|
||||
@@ -99,6 +99,9 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
|
||||
/// are _trying_ to receive a packet currently (note - we might just be waiting for one)
|
||||
bool isReceiving = false;
|
||||
|
||||
/// has the radio IRQ ever been armed? latches true and is never cleared, so ISR context only reads it
|
||||
volatile bool isrEverArmed = false;
|
||||
|
||||
protected:
|
||||
// Noise floor tracking - rolling window of samples.
|
||||
static const uint8_t NOISE_FLOOR_SAMPLES = 20;
|
||||
@@ -144,13 +147,26 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
|
||||
|
||||
/**
|
||||
* Glue functions called from ISR land
|
||||
*
|
||||
* Skip the detach until the IRQ has been armed once: the first setStandby() runs before any
|
||||
* enableInterrupt(), and ESP-IDF logs "GPIO isr service is not installed" for that call.
|
||||
*/
|
||||
virtual void disableInterrupt() = 0;
|
||||
void disableInterrupt()
|
||||
{
|
||||
if (!isrEverArmed)
|
||||
return;
|
||||
clearRadioIsr();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable a particular ISR callback glue function
|
||||
*/
|
||||
virtual void enableInterrupt(void (*)()) = 0;
|
||||
void enableInterrupt(void (*callback)())
|
||||
{
|
||||
// Latch before arming: the ISR can fire the moment the handler is installed.
|
||||
isrEverArmed = true;
|
||||
setRadioIsr(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll as a backup to catch missed edge-triggered interrupts.
|
||||
@@ -300,6 +316,10 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
|
||||
*/
|
||||
virtual void addReceiveMetadata(meshtastic_MeshPacket *mp) = 0;
|
||||
|
||||
/** Chip specific arm/disarm of the radio IRQ; call enableInterrupt()/disableInterrupt() instead */
|
||||
virtual void setRadioIsr(void (*callback)()) = 0;
|
||||
virtual void clearRadioIsr() = 0;
|
||||
|
||||
/**
|
||||
* Subclasses must override, implement and then call into this base class implementation
|
||||
*/
|
||||
@@ -311,23 +331,29 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
|
||||
template <typename T> uint32_t computePacketTime(T &lora, uint32_t pl, bool received)
|
||||
{
|
||||
if (received) {
|
||||
// First get the actual coding rate and CRC status from the received packet
|
||||
uint8_t rxCR;
|
||||
bool hasCRC;
|
||||
lora.getLoRaRxHeaderInfo(&rxCR, &hasCRC);
|
||||
// Go from raw header value to denominator
|
||||
if (rxCR < 5) {
|
||||
rxCR += 4;
|
||||
} else if (rxCR == 7) {
|
||||
rxCR = 8;
|
||||
}
|
||||
|
||||
// Received packet configuration must be the same as configured, except for coding rate and CRC
|
||||
DataRate_t dr = getDataRate();
|
||||
dr.lora.codingRate = rxCR;
|
||||
|
||||
PacketConfig_t pc = getPacketConfig();
|
||||
pc.lora.crcEnabled = hasCRC;
|
||||
|
||||
uint8_t rxCR = 0;
|
||||
bool hasCRC = true;
|
||||
if (lora.getLoRaRxHeaderInfo(&rxCR, &hasCRC) == RADIOLIB_ERR_NONE) {
|
||||
// Raw 0 is reserved and >7 is either undefined or an LR2021-only convolutional rate no
|
||||
// Meshtastic peer can send. calculateTimeOnAir() would multiply by it unchecked.
|
||||
if (rxCR < 1 || rxCR > 7) {
|
||||
LOG_WARN("Bogus RX coding rate %d from radio, use configured %d", rxCR, dr.lora.codingRate);
|
||||
} else {
|
||||
// Go from raw header value to denominator
|
||||
if (rxCR < 5) {
|
||||
rxCR += 4;
|
||||
} else if (rxCR == 7) {
|
||||
rxCR = 8;
|
||||
}
|
||||
|
||||
dr.lora.codingRate = rxCR;
|
||||
pc.lora.crcEnabled = hasCRC;
|
||||
}
|
||||
}
|
||||
|
||||
return lora.calculateTimeOnAir(modemType, dr, pc, pl) / 1000;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,12 @@
|
||||
*/
|
||||
ErrorCode ReliableRouter::send(meshtastic_MeshPacket *p)
|
||||
{
|
||||
if (isBlockedEventCoordinatePacket(p)) {
|
||||
LOG_DEBUG("Suppress reliable coordinate send on event (everyone) channel");
|
||||
packetPool.release(p);
|
||||
return meshtastic_Routing_Error_NOT_AUTHORIZED;
|
||||
}
|
||||
|
||||
const GlobalPacketId key(p);
|
||||
const bool retransmitting = p->want_ack;
|
||||
|
||||
|
||||
+110
-46
@@ -69,6 +69,52 @@ Allocator<meshtastic_MeshPacket> &packetPool = staticPool;
|
||||
|
||||
static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1] __attribute__((__aligned__));
|
||||
|
||||
static ChannelIndex getEffectiveChannelIndex(const meshtastic_MeshPacket *p)
|
||||
{
|
||||
ChannelIndex chIndex = p->channel;
|
||||
if (nodeDB && isFromUs(p) && !chIndex && !p->pki_encrypted && !isBroadcast(p->to)) {
|
||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(p->to);
|
||||
if (node)
|
||||
chIndex = node->channel;
|
||||
}
|
||||
return chIndex;
|
||||
}
|
||||
|
||||
bool isBlockedEventCoordinatePacket(const meshtastic_MeshPacket *p)
|
||||
{
|
||||
#if USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL
|
||||
if (p->pki_encrypted || willUsePki(p)) {
|
||||
return false;
|
||||
}
|
||||
if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
|
||||
return isCoordinatePortnum(p->decoded.portnum) && channels.isEventChannel(getEffectiveChannelIndex(p));
|
||||
}
|
||||
return false;
|
||||
#else
|
||||
(void)p;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool willUsePki(const meshtastic_MeshPacket *p)
|
||||
{
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||
if (p->which_payload_variant != meshtastic_MeshPacket_decoded_tag || !isFromUs(p))
|
||||
return false;
|
||||
bool haveDestKey = false;
|
||||
if (p->decoded.portnum == meshtastic_PortNum_KEY_VERIFICATION_APP) {
|
||||
meshtastic_NodeInfoLite_public_key_t destKey = {0, {0}};
|
||||
haveDestKey = nodeDB->copyPublicKey(p->to, destKey);
|
||||
if (!haveDestKey && p->pki_encrypted)
|
||||
haveDestKey = crypto->getPendingPublicKey(p->to, destKey);
|
||||
}
|
||||
return wouldEncryptWithPKC(p, getEffectiveChannelIndex(p), haveDestKey);
|
||||
#else
|
||||
(void)p;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
struct RoutingAuthCache {
|
||||
bool valid = false;
|
||||
// Deliberately NOT initialized in-class as this eats flash space.
|
||||
@@ -141,7 +187,6 @@ void resetRoutingAuthEvaluationCount()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
@@ -202,7 +247,7 @@ bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p)
|
||||
if (node && nodeInfoLiteIsFavorite(node) && nodeInfoLiteHasUser(node) &&
|
||||
IS_ONE_OF(node->role, meshtastic_Config_DeviceConfig_Role_ROUTER, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE,
|
||||
meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) {
|
||||
LOG_DEBUG("Identified unique favorite relay router 0x%08x from last byte 0x%x", resolved, p->relay_node);
|
||||
LOG_DEBUG("Unique favorite relay router 0x%08x from last byte 0x%x", resolved, p->relay_node);
|
||||
return false; // Don't decrement hop_limit
|
||||
}
|
||||
}
|
||||
@@ -223,7 +268,7 @@ int32_t Router::runOnce()
|
||||
perhapsHandleReceived(mp);
|
||||
}
|
||||
|
||||
// LOG_DEBUG("Sleep forever!");
|
||||
// LOG_DEBUG("Sleep forever");
|
||||
return INT32_MAX; // Wait a long time - until we get woken for the message queue
|
||||
}
|
||||
|
||||
@@ -266,14 +311,14 @@ PacketId generatePacketId()
|
||||
|
||||
rollingPacketId &= ID_COUNTER_MASK; // Mask out the top 22 bits
|
||||
PacketId id = rollingPacketId | random(UINT32_MAX & 0x7fffffff) << 10; // top 22 bits
|
||||
LOG_DEBUG("Partially randomized packet id %u", id);
|
||||
LOG_TRACE("Partially randomized packet id 0x%08x", id);
|
||||
return id;
|
||||
}
|
||||
|
||||
RxTimeStamp computeRxTimeStamp()
|
||||
{
|
||||
const bool haveTime = getRTCQuality() >= RTCQualityFromNet;
|
||||
return {haveTime ? getValidTime(RTCQualityFromNet) : Time::getMillis(), haveTime};
|
||||
return {haveTime ? getValidTime(RTCQualityFromNet) : Time::getUptimeSecs(), haveTime};
|
||||
}
|
||||
|
||||
void stampRxTime(meshtastic_MeshPacket *p)
|
||||
@@ -336,7 +381,7 @@ meshtastic_QueueStatus Router::getQueueStatus()
|
||||
ErrorCode Router::sendLocal(meshtastic_MeshPacket *p, RxSource src)
|
||||
{
|
||||
if (p->to == 0) {
|
||||
LOG_ERROR("Packet received with to: of 0!");
|
||||
LOG_ERROR("Packet received with to=0");
|
||||
}
|
||||
// No need to deliver externally if the destination is the local node
|
||||
if (isToUs(p)) {
|
||||
@@ -360,10 +405,10 @@ ErrorCode Router::sendLocal(meshtastic_MeshPacket *p, RxSource src)
|
||||
|
||||
// don't override if a channel was requested and no need to set it when PKI is enforced
|
||||
if (!p->channel && !p->pki_encrypted && !isBroadcast(p->to)) {
|
||||
meshtastic_NodeInfoLite const *node = nodeDB->getMeshNode(p->to);
|
||||
if (node) {
|
||||
p->channel = node->channel;
|
||||
LOG_DEBUG("localSend to channel %d", p->channel);
|
||||
ChannelIndex chIndex = getEffectiveChannelIndex(p);
|
||||
if (chIndex) {
|
||||
p->channel = chIndex;
|
||||
LOG_TRACE("localSend to channel %d", p->channel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,7 +430,7 @@ ErrorCode Router::sendLocal(meshtastic_MeshPacket *p, RxSource src)
|
||||
ErrorCode Router::send(meshtastic_MeshPacket *p)
|
||||
{
|
||||
if (isToUs(p)) {
|
||||
LOG_ERROR("BUG! send() called with packet destined for local node!");
|
||||
LOG_ERROR("BUG! send() with packet for local node");
|
||||
packetPool.release(p);
|
||||
return meshtastic_Routing_Error_BAD_REQUEST;
|
||||
} // should have already been handled by sendLocal
|
||||
@@ -397,7 +442,7 @@ ErrorCode Router::send(meshtastic_MeshPacket *p)
|
||||
if (hourlyTxPercent > effectiveDutyCycle) {
|
||||
uint8_t silentMinutes = airTime->getSilentMinutes(hourlyTxPercent, effectiveDutyCycle);
|
||||
|
||||
LOG_WARN("Duty cycle limit exceeded. Aborting send for now, you can send again in %d mins", silentMinutes);
|
||||
LOG_WARN("Duty cycle limit exceeded, abort send, retry in %d mins", silentMinutes);
|
||||
|
||||
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
||||
if (cn) {
|
||||
@@ -473,9 +518,15 @@ ErrorCode Router::send(meshtastic_MeshPacket *p)
|
||||
fixPriority(p); // Before encryption, fix the priority if it's unset
|
||||
// Position precision is an originator-only privacy policy. Relays keep
|
||||
// p->from as the original sender, so do not rewrite their POSITION_APP payload.
|
||||
if (isBlockedEventCoordinatePacket(p)) {
|
||||
LOG_DEBUG("Suppress coordinate send on event (everyone) channel");
|
||||
packetPool.release(p);
|
||||
return meshtastic_Routing_Error_NOT_AUTHORIZED;
|
||||
}
|
||||
|
||||
if (isFromUs(p)) {
|
||||
if (!applyPositionPrecisionForChannel(*p, p->channel)) {
|
||||
LOG_ERROR("Dropping malformed position packet before send");
|
||||
LOG_ERROR("Drop malformed position packet before send");
|
||||
packetPool.release(p);
|
||||
return meshtastic_Routing_Error_BAD_REQUEST;
|
||||
}
|
||||
@@ -650,20 +701,20 @@ bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p)
|
||||
if (!node)
|
||||
return false;
|
||||
nodeInfoLiteSetBit(node, NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_MASK, true);
|
||||
LOG_DEBUG("Verified XEdDSA signature from 0x%08x", p->from);
|
||||
LOG_TRACE("Verified XEdDSA signature from 0x%08x", p->from);
|
||||
} else {
|
||||
LOG_WARN("XEdDSA signature verification failed from 0x%08x, dropping", p->from);
|
||||
LOG_WARN("XEdDSA signature verify failed from 0x%08x, drop", p->from);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
const auto bootstrap = verifyFirstContactNodeInfo(p);
|
||||
if (bootstrap == NodeInfoBootstrapResult::INVALID) {
|
||||
LOG_WARN("Invalid first-contact XEdDSA NodeInfo from 0x%08x, dropping", p->from);
|
||||
LOG_WARN("Invalid first-contact XEdDSA NodeInfo from 0x%08x, drop", p->from);
|
||||
return false;
|
||||
}
|
||||
if (bootstrap == NodeInfoBootstrapResult::VERIFIED)
|
||||
return true;
|
||||
LOG_DEBUG("No public key for 0x%08x, cannot verify XEdDSA signature", p->from);
|
||||
LOG_DEBUG("No public key for 0x%08x, can't verify XEdDSA signature", p->from);
|
||||
if (strict)
|
||||
return false;
|
||||
}
|
||||
@@ -672,14 +723,13 @@ bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p)
|
||||
// senders emit only those two sizes (perhapsEncode sets 0 or XEDDSA_SIGNATURE_SIZE). Drop
|
||||
// it: a crafted partial signature would otherwise land in the unsigned branch below while
|
||||
// its bytes inflated the size estimate, letting a forged broadcast dodge the downgrade drop.
|
||||
LOG_WARN("Malformed XEdDSA signature (%u bytes) from 0x%08x, dropping", (unsigned)p->decoded.xeddsa_signature.size,
|
||||
p->from);
|
||||
LOG_WARN("Malformed XEdDSA signature (%u bytes) from 0x%08x, drop", (unsigned)p->decoded.xeddsa_signature.size, p->from);
|
||||
return false;
|
||||
} else {
|
||||
if (p->pki_encrypted)
|
||||
return true;
|
||||
if (strict) {
|
||||
LOG_WARN("Dropping unsigned packet from 0x%08x in Strict signature mode", p->from);
|
||||
LOG_WARN("Drop unsigned packet from 0x%08x in Strict signature mode", p->from);
|
||||
return false;
|
||||
}
|
||||
if (compatible)
|
||||
@@ -692,7 +742,7 @@ bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p)
|
||||
if (!canonicalSignableSize(&p->decoded, &canonicalSize))
|
||||
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) {
|
||||
LOG_WARN("Dropping unsigned packet from 0x%08x that previously signed", p->from);
|
||||
LOG_WARN("Drop unsigned packet from 0x%08x that previously signed", p->from);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -735,11 +785,11 @@ RoutingAuthVerdict passesRoutingAuthGate(meshtastic_MeshPacket *p)
|
||||
return RoutingAuthVerdict::REJECT;
|
||||
}
|
||||
if (state == DecodeState::DECODE_FATAL) {
|
||||
LOG_WARN("Fatal decode error, dropping packet");
|
||||
LOG_WARN("Fatal decode error, drop packet");
|
||||
return RoutingAuthVerdict::REJECT;
|
||||
}
|
||||
if (state == DecodeState::DECODE_FAILURE) {
|
||||
LOG_WARN("Decryptable packet failed decoding, dropping packet");
|
||||
LOG_WARN("Decryptable packet failed decoding, drop");
|
||||
return RoutingAuthVerdict::REJECT;
|
||||
}
|
||||
|
||||
@@ -804,7 +854,7 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
|
||||
if (config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_KNOWN_ONLY &&
|
||||
!nodeInfoLiteHasUser(nodeDB->getMeshNode(p->from))) {
|
||||
LOG_DEBUG("Node 0x%08x not in nodeDB-> Rebroadcast mode KNOWN_ONLY will ignore packet", p->from);
|
||||
LOG_DEBUG("Node 0x%08x not in nodeDB, Rebroadcast KNOWN_ONLY ignores packet", p->from);
|
||||
return DecodeState::DECODE_FAILURE;
|
||||
}
|
||||
|
||||
@@ -817,7 +867,7 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
|
||||
size_t rawSize = p->encrypted.size;
|
||||
if (rawSize > sizeof(bytes)) {
|
||||
LOG_ERROR("Packet too large to attempt decryption! (rawSize=%d > 256)", rawSize);
|
||||
LOG_ERROR("Packet too large to decrypt (rawSize=%d > 256)", rawSize);
|
||||
return DecodeState::DECODE_FATAL;
|
||||
}
|
||||
bool decrypted = false;
|
||||
@@ -834,7 +884,7 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
licensedPkiCandidate = true;
|
||||
} else if (pkiCandidate) {
|
||||
pkiAttempted = true;
|
||||
LOG_DEBUG("Attempt PKI decryption");
|
||||
LOG_TRACE("Attempt PKI decryption");
|
||||
// Resolve the sender's key only for actual PKI-decrypt candidates, not every encrypted channel
|
||||
// packet: copyPublicKeyForDecrypt() can fall through to a linear scan of TrafficManagement's large
|
||||
// NodeInfo cache. It returns authoritative keys (hot/warm), or a cold-tier cache key only when it is
|
||||
@@ -874,7 +924,7 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
adminKeyFallbackRefund();
|
||||
}
|
||||
if (decrypted) {
|
||||
LOG_INFO("PKI Decryption worked!");
|
||||
LOG_INFO("PKI Decryption worked");
|
||||
meshtastic_Data decodedtmp;
|
||||
memset(&decodedtmp, 0, sizeof(decodedtmp));
|
||||
size_t payloadSize = rawSize - MESHTASTIC_PKC_OVERHEAD;
|
||||
@@ -888,7 +938,7 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
}
|
||||
decrypted = true;
|
||||
rawSize = payloadSize; // commit the overhead subtraction only on full success
|
||||
LOG_INFO("Packet decrypted using PKI!");
|
||||
LOG_INFO("Packet decrypted using PKI");
|
||||
p->pki_encrypted = true;
|
||||
memcpy(p->public_key.bytes, remotePublic.bytes, 32);
|
||||
p->public_key.size = 32;
|
||||
@@ -906,7 +956,7 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
} else {
|
||||
// AEAD already authenticated this ciphertext, so no other candidate could decode it -
|
||||
// the payload is simply malformed.
|
||||
LOG_ERROR("PKC Decrypted, but pb_decode failed!");
|
||||
LOG_ERROR("PKC Decrypted, but pb_decode failed");
|
||||
return DecodeState::DECODE_FAILURE;
|
||||
}
|
||||
}
|
||||
@@ -960,6 +1010,11 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
return DecodeState::DECODE_POLICY_REJECT;
|
||||
#endif
|
||||
|
||||
if (isBlockedEventCoordinatePacket(p)) {
|
||||
LOG_DEBUG("Decoded coordinate packet on event channel; suppress payload logging");
|
||||
return DecodeState::DECODE_SUCCESS;
|
||||
}
|
||||
|
||||
if (p->decoded.has_bitfield)
|
||||
p->decoded.want_response |= p->decoded.bitfield & BITFIELD_WANT_RESPONSE_MASK;
|
||||
|
||||
@@ -1014,7 +1069,7 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
#endif
|
||||
return DecodeState::DECODE_SUCCESS;
|
||||
} else {
|
||||
LOG_WARN("No suitable channel found for decoding, hash was 0x%x!", p->channel);
|
||||
LOG_WARN("No channel found for decoding, hash 0x%x", p->channel);
|
||||
return (matchedChannel || pkiAttempted || licensedPkiCandidate) ? DecodeState::DECODE_FAILURE
|
||||
: DecodeState::DECODE_OPAQUE;
|
||||
}
|
||||
@@ -1091,7 +1146,7 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
|
||||
if (crypto->xeddsa_sign(p->from, p->id, p->decoded.portnum, p->decoded.payload.bytes, p->decoded.payload.size,
|
||||
p->decoded.xeddsa_signature.bytes)) {
|
||||
p->decoded.xeddsa_signature.size = XEDDSA_SIGNATURE_SIZE;
|
||||
LOG_DEBUG("XEdDSA signed packet 0x%08x", p->id);
|
||||
LOG_TRACE("XEdDSA signed packet 0x%08x", p->id);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1118,7 +1173,7 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
|
||||
// If the compressed length is greater than or equal to the original size, don't use the compressed form
|
||||
if (compressed_len >= p->decoded.payload.size) {
|
||||
|
||||
LOG_DEBUG("Not using compressing message");
|
||||
LOG_DEBUG("Not compressing");
|
||||
// Set the uncompressed payload variant anyway. Shouldn't hurt?
|
||||
// p->decoded.which_payloadVariant = Data_payload_tag;
|
||||
|
||||
@@ -1156,13 +1211,12 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
|
||||
// We may want to retool things so we can send a PKC packet when the client specifies a key and nodenum, even if the node
|
||||
// is not in the local nodedb
|
||||
if (wouldEncryptWithPKC(p, chIndex, haveDestKey)) {
|
||||
LOG_DEBUG("Use PKI!");
|
||||
LOG_DEBUG("Use PKI");
|
||||
if (numbytes + MESHTASTIC_HEADER_LENGTH + MESHTASTIC_PKC_OVERHEAD > MAX_LORA_PAYLOAD_LEN)
|
||||
return meshtastic_Routing_Error_TOO_LARGE;
|
||||
// Check for a usable public key for the destination (NodeDB or a pending key-verification key)
|
||||
if (!haveDestKey) {
|
||||
LOG_WARN("Unknown public key for destination node 0x%08x (portnum %d), refusing to send legacy DM", p->to,
|
||||
p->decoded.portnum);
|
||||
LOG_WARN("Unknown public key for 0x%08x (portnum %d), refuse legacy DM", p->to, p->decoded.portnum);
|
||||
return meshtastic_Routing_Error_PKI_SEND_FAIL_PUBLIC_KEY;
|
||||
}
|
||||
if (p->pki_encrypted && !memfll(p->public_key.bytes, 0, 32) && memcmp(p->public_key.bytes, destKey.bytes, 32) != 0) {
|
||||
@@ -1173,7 +1227,7 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
|
||||
// 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);
|
||||
LOG_WARN("PKI encryption failed for 0x%08x", p->to);
|
||||
return meshtastic_Routing_Error_PKI_FAILED;
|
||||
}
|
||||
numbytes += MESHTASTIC_PKC_OVERHEAD;
|
||||
@@ -1289,7 +1343,7 @@ void Router::deliverLocal(meshtastic_MeshPacket *p, RxSource src)
|
||||
// broadcast). Mirrors sendToPhone()'s degrade-on-exhaustion behavior.
|
||||
if (copy)
|
||||
packetPool.release(copy);
|
||||
LOG_WARN("Deferred local queue full/alloc failed, dropping loopback of 0x%08x", p->id);
|
||||
LOG_WARN("Deferred local queue full/alloc failed, drop loopback of 0x%08x", p->id);
|
||||
#ifdef PIO_UNIT_TESTING
|
||||
deferredLocalDropped++;
|
||||
#endif
|
||||
@@ -1372,8 +1426,8 @@ void Router::dispatchReceived(meshtastic_MeshPacket *p, RxSource src)
|
||||
// Fatal decoding error, we can't do anything with this packet
|
||||
LOG_WARN(decodedState == DecodeState::DECODE_POLICY_REJECT
|
||||
? "Packet rejected by signature policy"
|
||||
: (decodedState == DecodeState::DECODE_FATAL ? "Fatal decode error, dropping packet"
|
||||
: "Decryptable packet failed decoding, dropping packet"));
|
||||
: (decodedState == DecodeState::DECODE_FATAL ? "Fatal decode error, drop packet"
|
||||
: "Decryptable packet failed decoding, drop"));
|
||||
// A policy rejection is attacker-controlled input and must not cancel a valid pending
|
||||
// transmission with the same (from, id). Preserve the pre-existing fatal-decode behavior.
|
||||
if (decodedState == DecodeState::DECODE_FATAL)
|
||||
@@ -1404,7 +1458,7 @@ void Router::dispatchReceived(meshtastic_MeshPacket *p, RxSource src)
|
||||
if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
|
||||
p->decoded.portnum == meshtastic_PortNum_NEIGHBORINFO_APP &&
|
||||
(!moduleConfig.has_neighbor_info || !moduleConfig.neighbor_info.enabled)) {
|
||||
LOG_DEBUG("Neighbor info module is disabled, ignore neighbor packet");
|
||||
LOG_DEBUG("Neighbor info module disabled, ignore packet");
|
||||
cancelSending(p->from, p->id);
|
||||
skipHandle = true;
|
||||
}
|
||||
@@ -1416,7 +1470,7 @@ void Router::dispatchReceived(meshtastic_MeshPacket *p, RxSource src)
|
||||
p->decoded.portnum == meshtastic_PortNum_MESH_BEACON_APP &&
|
||||
(!moduleConfig.has_mesh_beacon ||
|
||||
!(moduleConfig.mesh_beacon.flags & meshtastic_ModuleConfig_MeshBeaconConfig_Flags_FLAG_LISTEN_ENABLED))) {
|
||||
LOG_DEBUG("Beacon listening is disabled, ignore beacon packet");
|
||||
LOG_DEBUG("Beacon listening disabled, ignore packet");
|
||||
cancelSending(p->from, p->id);
|
||||
skipHandle = true;
|
||||
}
|
||||
@@ -1438,6 +1492,16 @@ void Router::dispatchReceived(meshtastic_MeshPacket *p, RxSource src)
|
||||
cancelSending(p->from, p->id);
|
||||
skipHandle = true;
|
||||
}
|
||||
|
||||
#if USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL
|
||||
// Discard coordinate-bearing packets that arrive on the event ("everyone")
|
||||
// channel: don't process, store in NodeDB, or rebroadcast them.
|
||||
if (!skipHandle && isBlockedEventCoordinatePacket(p)) {
|
||||
LOG_DEBUG("Drop coordinate packet on event (everyone) channel");
|
||||
cancelSending(p->from, p->id);
|
||||
skipHandle = true;
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
printPacket("packet decoding failed or skipped (no PSK?)", p);
|
||||
}
|
||||
@@ -1449,7 +1513,7 @@ void Router::dispatchReceived(meshtastic_MeshPacket *p, RxSource src)
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_MQTT
|
||||
if (p_encrypted == nullptr) {
|
||||
LOG_WARN("p_encrypted is null, skipping MQTT publish");
|
||||
LOG_WARN("p_encrypted null, skip MQTT publish");
|
||||
} else {
|
||||
// Mark as pki_encrypted if it is not yet decoded and MQTT encryption is also enabled, hash matches and it's a DM not
|
||||
// to us (because we would be able to decrypt it)
|
||||
@@ -1469,7 +1533,7 @@ void Router::dispatchReceived(meshtastic_MeshPacket *p, RxSource src)
|
||||
if (encodeResult != meshtastic_Routing_Error_NONE) {
|
||||
// Encryption failed, release the new packet and fall back to sending the original encrypted packet to
|
||||
// MQTT
|
||||
LOG_WARN("Encryption of new TR packet failed, sending original TR to MQTT");
|
||||
LOG_WARN("New TR packet encrypt failed, send original TR to MQTT");
|
||||
packetPool.release(p_encrypted_new);
|
||||
p_encrypted_new = nullptr;
|
||||
} else {
|
||||
@@ -1479,7 +1543,7 @@ void Router::dispatchReceived(meshtastic_MeshPacket *p, RxSource src)
|
||||
}
|
||||
} else {
|
||||
// Allocation failed, log a warning and fall back to sending the original encrypted packet to MQTT
|
||||
LOG_WARN("Failed to allocate new encrypted packet for TR, sending original TR to MQTT");
|
||||
LOG_WARN("Alloc encrypted TR packet failed, send original TR to MQTT");
|
||||
}
|
||||
}
|
||||
mqtt->onSend(*p_encrypted, *p, p->channel);
|
||||
@@ -1504,7 +1568,7 @@ void Router::perhapsHandleReceived(meshtastic_MeshPacket *p)
|
||||
// assert(radioConfig.has_preferences);
|
||||
if (is_in_repeated(config.lora.ignore_incoming, p->from)) {
|
||||
clearRoutingAuthCache();
|
||||
LOG_DEBUG("Ignore msg, 0x%08x is in our ignore list", p->from);
|
||||
LOG_DEBUG("Ignore msg, 0x%08x in ignore list", p->from);
|
||||
packetPool.release(p);
|
||||
return;
|
||||
}
|
||||
@@ -1554,7 +1618,7 @@ void Router::perhapsHandleReceived(meshtastic_MeshPacket *p)
|
||||
|
||||
if (shouldFilterReceived(p)) {
|
||||
clearRoutingAuthCache();
|
||||
LOG_DEBUG("Incoming msg was filtered from 0x%08x", p->from);
|
||||
LOG_DEBUG("Incoming msg filtered from 0x%08x", p->from);
|
||||
packetPool.release(p);
|
||||
return;
|
||||
}
|
||||
|
||||
+11
-1
@@ -11,8 +11,18 @@
|
||||
#include "concurrency/OSThread.h"
|
||||
#include <memory>
|
||||
|
||||
inline bool isCoordinatePortnum(meshtastic_PortNum portnum)
|
||||
{
|
||||
return portnum == meshtastic_PortNum_POSITION_APP || portnum == meshtastic_PortNum_WAYPOINT_APP ||
|
||||
portnum == meshtastic_PortNum_MAP_REPORT_APP;
|
||||
}
|
||||
|
||||
bool isBlockedEventCoordinatePacket(const meshtastic_MeshPacket *p);
|
||||
bool willUsePki(const meshtastic_MeshPacket *p);
|
||||
|
||||
/// rx_time/has_rx_time for "now": a real epoch when the clock is trustworthy, else a
|
||||
/// Time::getMillis() placeholder with valid=false.
|
||||
/// Time::getUptimeSecs() placeholder with valid=false. Uptime seconds are monotonic, so
|
||||
/// reconciliation against a later epoch is exact at any age.
|
||||
struct RxTimeStamp {
|
||||
uint32_t time;
|
||||
bool valid;
|
||||
|
||||
@@ -77,9 +77,9 @@ template <typename T> bool SX126xInterface<T>::init()
|
||||
}
|
||||
#endif
|
||||
if (tcxoVoltage == 0.0)
|
||||
LOG_DEBUG("SX126X_DIO3_TCXO_VOLTAGE not defined, not using DIO3 as TCXO reference voltage");
|
||||
LOG_DEBUG("SX126X_DIO3_TCXO_VOLTAGE not defined, DIO3 not used as TCXO Vref");
|
||||
else
|
||||
LOG_DEBUG("SX126X_DIO3_TCXO_VOLTAGE defined, using DIO3 as TCXO reference voltage at %f V", tcxoVoltage);
|
||||
LOG_DEBUG("SX126X_DIO3_TCXO_VOLTAGE defined, DIO3 as TCXO Vref %f V", tcxoVoltage);
|
||||
setTransmitEnable(false);
|
||||
// FIXME: May want to set depending on a definition, currently all SX126x variant files use the DC-DC regulator option
|
||||
bool useRegulatorLDO = false; // Seems to depend on the connection to pin 9/DCC_SW - if an inductor DCDC?
|
||||
@@ -139,21 +139,21 @@ template <typename T> bool SX126xInterface<T>::init()
|
||||
// no effect
|
||||
#if ARCH_PORTDUINO
|
||||
if (res == RADIOLIB_ERR_NONE) {
|
||||
LOG_DEBUG("Use MCU pin %i as RXEN and pin %i as TXEN to control RF switching", portduino_config.lora_rxen_pin.pin,
|
||||
LOG_DEBUG("Use MCU pin %i as RXEN, pin %i as TXEN for RF switching", portduino_config.lora_rxen_pin.pin,
|
||||
portduino_config.lora_txen_pin.pin);
|
||||
lora.setRfSwitchPins(portduino_config.lora_rxen_pin.pin, portduino_config.lora_txen_pin.pin);
|
||||
}
|
||||
#else
|
||||
#ifndef SX126X_RXEN
|
||||
#define SX126X_RXEN RADIOLIB_NC
|
||||
LOG_DEBUG("SX126X_RXEN not defined, defaulting to RADIOLIB_NC");
|
||||
LOG_DEBUG("SX126X_RXEN not defined, default RADIOLIB_NC");
|
||||
#endif
|
||||
#ifndef SX126X_TXEN
|
||||
#define SX126X_TXEN RADIOLIB_NC
|
||||
LOG_DEBUG("SX126X_TXEN not defined, defaulting to RADIOLIB_NC");
|
||||
LOG_DEBUG("SX126X_TXEN not defined, default RADIOLIB_NC");
|
||||
#endif
|
||||
if (res == RADIOLIB_ERR_NONE) {
|
||||
LOG_DEBUG("Use MCU pin %i as RXEN and pin %i as TXEN to control RF switching", SX126X_RXEN, SX126X_TXEN);
|
||||
LOG_DEBUG("Use MCU pin %i as RXEN, pin %i as TXEN for RF switching", SX126X_RXEN, SX126X_TXEN);
|
||||
lora.setRfSwitchPins(SX126X_RXEN, SX126X_TXEN);
|
||||
}
|
||||
#endif
|
||||
@@ -162,15 +162,15 @@ template <typename T> bool SX126xInterface<T>::init()
|
||||
LOG_INFO("Set RX gain to boosted mode; result: %d", result);
|
||||
} else {
|
||||
uint16_t result = lora.setRxBoostedGainMode(false);
|
||||
LOG_INFO("Set RX gain to power saving mode (boosted mode off); result: %d", result);
|
||||
LOG_INFO("Set RX gain to power saving mode; result: %d", result);
|
||||
}
|
||||
|
||||
// Undocumented SX1262 register patch recommended by Heltec/Semtech for improved RX sensitivity.
|
||||
// Sets bit 0 of register 0x8B5.
|
||||
if (module.SPIsetRegValue(0x8B5, 0x01, 0, 0) == RADIOLIB_ERR_NONE) {
|
||||
LOG_INFO("Applied SX1262 register 0x8B5 patch for RX improvement");
|
||||
LOG_INFO("Applied SX1262 reg 0x8B5 RX patch");
|
||||
} else {
|
||||
LOG_WARN("Failed to apply SX1262 register 0x8B5 patch for RX improvement");
|
||||
LOG_WARN("Can't apply SX1262 reg 0x8B5 RX patch");
|
||||
}
|
||||
|
||||
if (res == RADIOLIB_ERR_NONE)
|
||||
@@ -230,7 +230,7 @@ template <typename T> bool SX126xInterface<T>::reconfigure()
|
||||
if (err != RADIOLIB_ERR_NONE) {
|
||||
// Don't abort: this power is operator config (tx_power/SX126X_MAX_POWER); a value above the
|
||||
// driver's max would crash the daemon before reloadConfig() persists. Flag it and keep prior power.
|
||||
LOG_ERROR("SX126X setOutputPower %d dBm rejected (%s%d); keeping previous Tx power", power, radioLibErr, err);
|
||||
LOG_ERROR("SX126X setOutputPower %d dBm rejected (%s%d); keep previous Tx power", power, radioLibErr, err);
|
||||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
||||
}
|
||||
|
||||
@@ -250,7 +250,7 @@ template <typename T> int16_t SX126xInterface<T>::getCurrentRSSI()
|
||||
return (int16_t)round(rssi);
|
||||
}
|
||||
|
||||
template <typename T> void SX126xInterface<T>::enableInterrupt(void (*callback)())
|
||||
template <typename T> void SX126xInterface<T>::setRadioIsr(void (*callback)())
|
||||
{
|
||||
#ifdef LORA_DIO1_SOFTWARE_POLL
|
||||
irqPollingActive = true;
|
||||
@@ -261,7 +261,7 @@ template <typename T> void SX126xInterface<T>::enableInterrupt(void (*callback)(
|
||||
#endif
|
||||
}
|
||||
|
||||
template <typename T> void SX126xInterface<T>::disableInterrupt()
|
||||
template <typename T> void SX126xInterface<T>::clearRadioIsr()
|
||||
{
|
||||
#ifdef LORA_DIO1_SOFTWARE_POLL
|
||||
irqPollingActive = false;
|
||||
@@ -336,7 +336,7 @@ template <typename T> void SX126xInterface<T>::addReceiveMetadata(meshtastic_Mes
|
||||
mp->rx_snr = lora.getSNR();
|
||||
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_TRACE("Corrected frequency offset: %f", lora.getFrequencyError());
|
||||
}
|
||||
|
||||
/** We override to turn on transmitter power as needed.
|
||||
@@ -478,7 +478,7 @@ template <typename T> void SX126xInterface<T>::resetAGC()
|
||||
}
|
||||
|
||||
if (module.hal->digitalRead(module.getGpio())) {
|
||||
LOG_WARN("SX126x AGC reset: calibration did not complete within 50ms");
|
||||
LOG_WARN("SX126x AGC reset: calibration not done in 50ms");
|
||||
startReceive();
|
||||
return;
|
||||
}
|
||||
@@ -506,7 +506,7 @@ template <typename T> void SX126xInterface<T>::resetAGC()
|
||||
// Without this re-apply, every SX1262 node loses its RX boost ~60s after boot
|
||||
// and never recovers until reboot. See empirical evidence in the PR description.
|
||||
if (module.SPIsetRegValue(0x8B5, 0x01, 0, 0) != RADIOLIB_ERR_NONE) {
|
||||
LOG_WARN("SX126x resetAGC: failed to re-apply 0x8B5 RX sensitivity patch");
|
||||
LOG_WARN("SX126x resetAGC: 0x8B5 RX patch re-apply failed");
|
||||
}
|
||||
|
||||
// 6. Resume receiving
|
||||
|
||||
@@ -47,12 +47,12 @@ template <class T> class SX126xInterface : public RadioLibInterface
|
||||
/**
|
||||
* Glue functions called from ISR land
|
||||
*/
|
||||
virtual void disableInterrupt() override;
|
||||
virtual void clearRadioIsr() override;
|
||||
|
||||
/**
|
||||
* Enable a particular ISR callback glue function
|
||||
*/
|
||||
virtual void enableInterrupt(void (*callback)()) override;
|
||||
virtual void setRadioIsr(void (*callback)()) override;
|
||||
|
||||
#ifdef LORA_DIO1_SOFTWARE_POLL
|
||||
void handleSoftwareLoraIrqPoll() override;
|
||||
|
||||
@@ -156,7 +156,7 @@ template <typename T> bool SX128xInterface<T>::reconfigure()
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T> void SX128xInterface<T>::disableInterrupt()
|
||||
template <typename T> void SX128xInterface<T>::clearRadioIsr()
|
||||
{
|
||||
lora.clearDio1Action();
|
||||
}
|
||||
|
||||
@@ -43,12 +43,12 @@ template <class T> class SX128xInterface : public RadioLibInterface
|
||||
/**
|
||||
* Glue functions called from ISR land
|
||||
*/
|
||||
virtual void disableInterrupt() override;
|
||||
virtual void clearRadioIsr() override;
|
||||
|
||||
/**
|
||||
* Enable a particular ISR callback glue function
|
||||
*/
|
||||
virtual void enableInterrupt(void (*callback)()) { lora.setDio1Action(callback); }
|
||||
virtual void setRadioIsr(void (*callback)()) override { lora.setDio1Action(callback); }
|
||||
|
||||
/** can we detect a LoRa preamble on the current channel? */
|
||||
virtual bool isChannelActive() override;
|
||||
|
||||
+12
-3
@@ -1,4 +1,5 @@
|
||||
#include "Throttle.h"
|
||||
#include "UptimeClock.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
/// @brief Execute a function throttled to a minimum interval
|
||||
@@ -10,11 +11,11 @@
|
||||
bool Throttle::execute(uint32_t *lastExecutionMs, uint32_t minumumIntervalMs, void (*throttleFunc)(void), void (*onDefer)(void))
|
||||
{
|
||||
if (*lastExecutionMs == 0) {
|
||||
*lastExecutionMs = millis();
|
||||
*lastExecutionMs = Time::getMillis();
|
||||
throttleFunc();
|
||||
return true;
|
||||
}
|
||||
uint32_t now = millis();
|
||||
uint32_t now = Time::getMillis();
|
||||
|
||||
if ((now - *lastExecutionMs) >= minumumIntervalMs) {
|
||||
throttleFunc();
|
||||
@@ -31,6 +32,14 @@ bool Throttle::execute(uint32_t *lastExecutionMs, uint32_t minumumIntervalMs, vo
|
||||
/// @param timeSpanMs The interval in milliseconds of the timespan
|
||||
bool Throttle::isWithinTimespanMs(uint32_t lastExecutionMs, uint32_t timeSpanMs)
|
||||
{
|
||||
uint32_t now = millis();
|
||||
uint32_t now = Time::getMillis();
|
||||
return (now - lastExecutionMs) < timeSpanMs;
|
||||
}
|
||||
|
||||
/// @brief Check whether an absolute deadline has arrived, correctly across the millis() wrap
|
||||
/// @param deadlineMs The deadline, as a millis() value
|
||||
/// See the header for the range limit and the sentinel requirement.
|
||||
bool Throttle::deadlinePassed(uint32_t deadlineMs)
|
||||
{
|
||||
return deadlinePassedAt(Time::getMillis(), deadlineMs);
|
||||
}
|
||||
@@ -7,4 +7,48 @@ class Throttle
|
||||
public:
|
||||
static bool execute(uint32_t *lastExecutionMs, uint32_t minumumIntervalMs, void (*func)(void), void (*onDefer)(void) = NULL);
|
||||
static bool isWithinTimespanMs(uint32_t lastExecutionMs, uint32_t intervalMs);
|
||||
|
||||
/// Complement of isWithinTimespanMs(): true once intervalMs has passed since lastExecutionMs.
|
||||
/// Boundary is inclusive (>=), mirroring isWithinTimespanMs()'s exclusive <.
|
||||
/// Deliberately does not treat lastExecutionMs == 0 as "never run" - callers that use 0 as a
|
||||
/// sentinel must test for it separately, so the sentinel never reaches the arithmetic.
|
||||
static bool hasElapsed(uint32_t lastExecutionMs, uint32_t intervalMs)
|
||||
{
|
||||
return !isWithinTimespanMs(lastExecutionMs, intervalMs);
|
||||
}
|
||||
|
||||
/// True once an absolute deadline has arrived. Use this rather than comparing against millis()
|
||||
/// directly: that inverts while the deadline sits on the far side of the 32-bit wrap, so the
|
||||
/// action either fires immediately or blocks for about the interval it should have waited.
|
||||
///
|
||||
/// Use this when the site stores a deadline; use hasElapsed() when it stores the time of the
|
||||
/// last event, which allows the full ~49.7 day range instead of ~24.8 days ahead.
|
||||
///
|
||||
/// Callers that overload the deadline with an "inactive" sentinel (0, or UINT32_MAX) MUST test
|
||||
/// for that separately, first: every such value is arithmetically far in the past, so it reads
|
||||
/// as passed.
|
||||
///
|
||||
/// TODO(deadline-type): mistake-proof that MUST by giving a deadline its own one-field type -
|
||||
/// Deadline::in(ms) / .armed() / .passed() / .disarm(). A hand-built `now + interval` could then
|
||||
/// no longer land on the sentinel by accident, and "armed" would stay a question separate from
|
||||
/// "passed" - the split that has to survive, because which way "inactive" falls is the caller's
|
||||
/// to decide. Same size and cost as the bare uint32_t. The conversion sites, grouped by the four
|
||||
/// meanings they give the sentinel today:
|
||||
/// 0 = unarmed - Power.cpp rebootAtMsec/shutdownAtMsec (the cheapest pair to convert), and
|
||||
/// GPS.cpp fixHoldEnds, whose arm site remaps a 0 result to 1 by hand.
|
||||
/// 0 = forever - NotificationRenderer.cpp alertBannerUntil. Every read spells its own `> 0`
|
||||
/// guard, so this third state wants naming rather than repeating.
|
||||
/// 0 = due now - ethClient.cpp ntp_renew, forced at link-up.
|
||||
/// UINT32_MAX - ExternalNotificationModule.cpp nagCycleCutoff, whose armed() also lives in a
|
||||
/// second variable (isNagging) and whose arm site can land on the sentinel.
|
||||
static bool deadlinePassed(uint32_t deadlineMs);
|
||||
|
||||
/// deadlinePassed() against a caller-supplied "now", for a loop that snapshots the time once and
|
||||
/// tests many deadlines against it. Same range limit and sentinel rules as above.
|
||||
static bool deadlinePassedAt(uint32_t nowMs, uint32_t deadlineMs)
|
||||
{
|
||||
// Passed iff now - deadline has not wrapped past 2^31 ms; further-ahead deadlines land in
|
||||
// the top half. Not an int32_t cast, which is implementation-defined beyond INT32_MAX.
|
||||
return (uint32_t)(nowMs - deadlineMs) < 0x80000000u;
|
||||
}
|
||||
};
|
||||
@@ -494,7 +494,7 @@ void WarmNodeStore::load()
|
||||
bool WarmNodeStore::save()
|
||||
{
|
||||
if (!powerHAL_isPowerLevelSafe()) {
|
||||
LOG_ERROR("Error: trying to save WarmStore on unsafe device power level.");
|
||||
LOG_ERROR("Trying to save WarmStore on unsafe device power level");
|
||||
return false;
|
||||
}
|
||||
concurrency::LockGuard g(spiLock);
|
||||
@@ -605,7 +605,7 @@ bool WarmNodeStore::save()
|
||||
if (!entries)
|
||||
return false;
|
||||
if (!powerHAL_isPowerLevelSafe()) {
|
||||
LOG_ERROR("Error: trying to save WarmStore on unsafe device power level.");
|
||||
LOG_ERROR("Trying to save WarmStore on unsafe device power level");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ bool PacketAPI::receivePacket(void)
|
||||
}
|
||||
break;
|
||||
default:
|
||||
LOG_ERROR("Error: unhandled meshtastic_ToRadio variant: %d", mr->which_payload_variant);
|
||||
LOG_ERROR("Unhandled meshtastic_ToRadio variant: %d", mr->which_payload_variant);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,7 +270,7 @@ bool ensureCertForIp(IPAddress ip, EthCertMaterial &out)
|
||||
(unsigned)out.keyDer.size(), ipStr.c_str());
|
||||
return true;
|
||||
}
|
||||
LOG_WARN("ETH CERT: cached cert/key failed to parse (partial write?), regenerating");
|
||||
LOG_WARN("ETH CERT: cached cert/key parse failed (partial write?), regen");
|
||||
out.certDer.clear();
|
||||
out.keyDer.clear();
|
||||
}
|
||||
@@ -279,7 +279,7 @@ bool ensureCertForIp(IPAddress ip, EthCertMaterial &out)
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("ETH CERT: generating ECDSA P-256 self-signed cert for IP %s...", ipStr.c_str());
|
||||
LOG_INFO("ETH CERT: gen ECDSA P-256 self-signed cert for IP %s", ipStr.c_str());
|
||||
uint32_t t0 = millis();
|
||||
if (!generateCert(ip, out)) {
|
||||
LOG_ERROR("ETH CERT: generation failed");
|
||||
@@ -299,7 +299,7 @@ bool ensureCertForIp(IPAddress ip, EthCertMaterial &out)
|
||||
writeText(IP_PATH, "");
|
||||
if (!writeBinary(CERT_PATH, out.certDer.data(), out.certDer.size()) ||
|
||||
!writeBinary(KEY_PATH, out.keyDer.data(), out.keyDer.size()) || !writeText(IP_PATH, ipStr)) {
|
||||
LOG_WARN("ETH CERT: persist failed - will regenerate next boot");
|
||||
LOG_WARN("ETH CERT: persist failed, regen next boot");
|
||||
} else {
|
||||
LOG_INFO("ETH CERT: persisted to LittleFS");
|
||||
}
|
||||
@@ -337,7 +337,7 @@ class EthCertThread : public concurrency::OSThread
|
||||
// regenerates whenever its saved IP != ip, so the cert SAN follows.
|
||||
bool ok = ensureCertForIp(ip, material_);
|
||||
if (!ok) {
|
||||
LOG_ERROR("ETH CERT: pipeline FAILED - TLS server will not start");
|
||||
LOG_ERROR("ETH CERT: pipeline FAILED, no TLS server");
|
||||
// Don't leave isReady() reporting true with empty material: a later TLS
|
||||
// teardown (e.g. a W5500 reset) would then fail initTlsContext() and stay
|
||||
// disabled. Clear readiness so the TLS worker waits and the next poll
|
||||
@@ -375,7 +375,7 @@ void initEthCertThread()
|
||||
if (certThread)
|
||||
return;
|
||||
certThread = new EthCertThread();
|
||||
LOG_INFO("ETH CERT: deferred worker scheduled (waits for DHCP, runs once)");
|
||||
LOG_INFO("ETH CERT: deferred worker scheduled (awaits DHCP)");
|
||||
}
|
||||
|
||||
bool isEthCertReady()
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "configuration.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "main.h"
|
||||
#include "mesh/Throttle.h"
|
||||
#include "mesh/api/ethServerAPI.h"
|
||||
#include "target_specific.h"
|
||||
#if HAS_ETHERNET && defined(HAS_ETHERNET_OTA)
|
||||
@@ -196,7 +197,9 @@ static int32_t reconnectETH()
|
||||
}
|
||||
|
||||
#ifndef DISABLE_NTP
|
||||
if (isEthernetAvailable() && (ntp_renew < millis())) {
|
||||
// 0 here means "renew now" (forced at link-up). deadlinePassed(0) only reads as passed for the
|
||||
// first half of each wrap cycle, so treat 0 as always-due rather than relying on that.
|
||||
if (isEthernetAvailable() && (ntp_renew == 0 || Throttle::deadlinePassed(ntp_renew))) {
|
||||
|
||||
LOG_INFO("Update NTP time from %s", config.network.ntp_server);
|
||||
if (timeClient.update()) {
|
||||
|
||||
@@ -99,7 +99,7 @@ static bool authenticateClient(EthernetClient &client)
|
||||
// Rate-limit after failed auth - close silently so the error byte is not
|
||||
// misinterpreted as part of the nonce by a re-trying client.
|
||||
if (lastAuthFailure != 0 && (millis() - lastAuthFailure) < OTA_AUTH_COOLDOWN_MS) {
|
||||
LOG_WARN("ETH OTA: Auth cooldown active, rejecting connection");
|
||||
LOG_WARN("ETH OTA: Auth cooldown, reject connection");
|
||||
client.stop();
|
||||
return false;
|
||||
}
|
||||
@@ -260,7 +260,7 @@ static void handleOTAClient(EthernetClient &client)
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("ETH OTA: Update staged successfully (%u bytes). Rebooting...", hdr.firmwareSize);
|
||||
LOG_INFO("ETH OTA: Update staged (%u bytes). Rebooting", hdr.firmwareSize);
|
||||
client.write(OTA_OK);
|
||||
client.flush();
|
||||
delay(500);
|
||||
|
||||
@@ -30,7 +30,7 @@ PB_BIND(meshtastic_SharedContact, meshtastic_SharedContact, AUTO)
|
||||
PB_BIND(meshtastic_KeyVerificationAdmin, meshtastic_KeyVerificationAdmin, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_SensorConfig, meshtastic_SensorConfig, AUTO)
|
||||
PB_BIND(meshtastic_SensorConfig, meshtastic_SensorConfig, 2)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_SCD4X_config, meshtastic_SCD4X_config, AUTO)
|
||||
@@ -39,12 +39,18 @@ PB_BIND(meshtastic_SCD4X_config, meshtastic_SCD4X_config, AUTO)
|
||||
PB_BIND(meshtastic_SEN5X_config, meshtastic_SEN5X_config, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_SEN6X_config, meshtastic_SEN6X_config, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_SCD30_config, meshtastic_SCD30_config, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_SHTXX_config, meshtastic_SHTXX_config, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_DS248X_config, meshtastic_DS248X_config, AUTO)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@ typedef struct _meshtastic_LockdownAuth {
|
||||
token at unlock time: the client-supplied boots_remaining when
|
||||
non-zero, otherwise the firmware default (TOKEN_DEFAULT_BOOTS).
|
||||
Note that boots_remaining == 0 in this message means "use firmware
|
||||
default", NOT "zero boots" - a client computing the ceiling for
|
||||
default", NOT "zero boots" — a client computing the ceiling for
|
||||
display should mirror that resolution rather than multiplying the
|
||||
raw request value.
|
||||
|
||||
@@ -196,7 +196,7 @@ typedef struct _meshtastic_LockdownAuth {
|
||||
|
||||
Uses millis() (CPU uptime), not wall-clock time, so the cap is
|
||||
immune to GPS spoofing, RTC backup-battery removal, and Faraday
|
||||
cage isolation - none of those move the uptime counter. The only
|
||||
cage isolation — none of those move the uptime counter. The only
|
||||
way to reset the session clock is a reboot, which costs a boot
|
||||
from the on-flash, HMAC-bound counter. */
|
||||
uint32_t max_session_seconds;
|
||||
@@ -213,7 +213,7 @@ typedef struct _meshtastic_LockdownAuth {
|
||||
|
||||
NOT reversed by this operation: APPROTECT. Once the debug port
|
||||
lockout has been burned (on silicon where it is effective) it is
|
||||
permanent - disabling lockdown decrypts your data and removes the
|
||||
permanent — disabling lockdown decrypts your data and removes the
|
||||
access gates, but the SWD/JTAG port stays locked for the life of
|
||||
the device (recoverable only via a full chip erase over a debug
|
||||
probe, which destroys all data). Clients should make this
|
||||
@@ -303,8 +303,38 @@ typedef struct _meshtastic_SEN5X_config {
|
||||
/* One-shot mode (true for low power - one-shot mode, false for normal - continuous mode) */
|
||||
bool has_set_one_shot_mode;
|
||||
bool set_one_shot_mode;
|
||||
/* Trigger a fan cleaning cycle */
|
||||
bool has_start_fan_cleaning;
|
||||
bool start_fan_cleaning;
|
||||
} meshtastic_SEN5X_config;
|
||||
|
||||
typedef struct _meshtastic_SEN6X_config {
|
||||
/* Reference temperature in degC */
|
||||
bool has_set_temperature;
|
||||
float set_temperature;
|
||||
/* One-shot mode (true for low power - one-shot mode, false for normal - continuous mode) */
|
||||
bool has_set_one_shot_mode;
|
||||
bool set_one_shot_mode;
|
||||
/* Trigger a fan cleaning cycle */
|
||||
bool has_start_fan_cleaning;
|
||||
bool start_fan_cleaning;
|
||||
/* Set Automatic self-calibration enabled (CO2-capable variants only: SEN63C, SEN66, SEN69C) */
|
||||
bool has_set_asc;
|
||||
bool set_asc;
|
||||
/* Recalibration target CO2 concentration in ppm (FRC only), CO2-capable variants only */
|
||||
bool has_set_target_co2_conc;
|
||||
uint32_t set_target_co2_conc;
|
||||
/* Altitude of sensor in meters above sea level. 0 - 3000m (overrides ambient pressure), CO2-capable variants only */
|
||||
bool has_set_altitude;
|
||||
uint32_t set_altitude;
|
||||
/* Sensor ambient pressure in Pa. 70000 - 120000 Pa (overrides altitude), CO2-capable variants only */
|
||||
bool has_set_ambient_pressure;
|
||||
uint32_t set_ambient_pressure;
|
||||
/* Perform a factory reset of the CO2 sensor's calibration, CO2-capable variants only */
|
||||
bool has_factory_reset;
|
||||
bool factory_reset;
|
||||
} meshtastic_SEN6X_config;
|
||||
|
||||
typedef struct _meshtastic_SCD30_config {
|
||||
/* Set Automatic self-calibration enabled */
|
||||
bool has_set_asc;
|
||||
@@ -332,6 +362,12 @@ typedef struct _meshtastic_SHTXX_config {
|
||||
uint32_t set_accuracy;
|
||||
} meshtastic_SHTXX_config;
|
||||
|
||||
typedef struct _meshtastic_DS248X_config {
|
||||
/* Main channel for temperature reporting (0-7) */
|
||||
bool has_main_temperature_channel;
|
||||
uint32_t main_temperature_channel;
|
||||
} meshtastic_DS248X_config;
|
||||
|
||||
typedef struct _meshtastic_SensorConfig {
|
||||
/* SCD4X CO2 Sensor configuration */
|
||||
bool has_scd4x_config;
|
||||
@@ -345,6 +381,12 @@ typedef struct _meshtastic_SensorConfig {
|
||||
/* SHTXX temperature and relative humidity sensor configuration */
|
||||
bool has_shtxx_config;
|
||||
meshtastic_SHTXX_config shtxx_config;
|
||||
/* DS248X-800 temperature sensor configuration */
|
||||
bool has_ds248x_config;
|
||||
meshtastic_DS248X_config ds248x_config;
|
||||
/* SEN6X PM/RHT/VOC/NOx/CO2/HCHO Sensor configuration */
|
||||
bool has_sen6x_config;
|
||||
meshtastic_SEN6X_config sen6x_config;
|
||||
} meshtastic_SensorConfig;
|
||||
|
||||
typedef PB_BYTES_ARRAY_T(8) meshtastic_AdminMessage_session_passkey_t;
|
||||
@@ -544,6 +586,8 @@ extern "C" {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* Initializer values for message structs */
|
||||
#define meshtastic_AdminMessage_init_default {0, {0}, {0, {0}}}
|
||||
#define meshtastic_AdminMessage_InputEvent_init_default {0, 0, 0, 0}
|
||||
@@ -553,11 +597,13 @@ extern "C" {
|
||||
#define meshtastic_NodeRemoteHardwarePinsResponse_init_default {0, {meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default}}
|
||||
#define meshtastic_SharedContact_init_default {0, false, meshtastic_User_init_default, 0, 0}
|
||||
#define meshtastic_KeyVerificationAdmin_init_default {_meshtastic_KeyVerificationAdmin_MessageType_MIN, 0, 0, false, 0}
|
||||
#define meshtastic_SensorConfig_init_default {false, meshtastic_SCD4X_config_init_default, false, meshtastic_SEN5X_config_init_default, false, meshtastic_SCD30_config_init_default, false, meshtastic_SHTXX_config_init_default}
|
||||
#define meshtastic_SensorConfig_init_default {false, meshtastic_SCD4X_config_init_default, false, meshtastic_SEN5X_config_init_default, false, meshtastic_SCD30_config_init_default, false, meshtastic_SHTXX_config_init_default, false, meshtastic_DS248X_config_init_default, false, meshtastic_SEN6X_config_init_default}
|
||||
#define meshtastic_SCD4X_config_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||
#define meshtastic_SEN5X_config_init_default {false, 0, false, 0}
|
||||
#define meshtastic_SEN5X_config_init_default {false, 0, false, 0, false, 0}
|
||||
#define meshtastic_SEN6X_config_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||
#define meshtastic_SCD30_config_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||
#define meshtastic_SHTXX_config_init_default {false, 0}
|
||||
#define meshtastic_DS248X_config_init_default {false, 0}
|
||||
#define meshtastic_AdminMessage_init_zero {0, {0}, {0, {0}}}
|
||||
#define meshtastic_AdminMessage_InputEvent_init_zero {0, 0, 0, 0}
|
||||
#define meshtastic_AdminMessage_OTAEvent_init_zero {_meshtastic_OTAMode_MIN, {0, {0}}}
|
||||
@@ -566,11 +612,13 @@ extern "C" {
|
||||
#define meshtastic_NodeRemoteHardwarePinsResponse_init_zero {0, {meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero}}
|
||||
#define meshtastic_SharedContact_init_zero {0, false, meshtastic_User_init_zero, 0, 0}
|
||||
#define meshtastic_KeyVerificationAdmin_init_zero {_meshtastic_KeyVerificationAdmin_MessageType_MIN, 0, 0, false, 0}
|
||||
#define meshtastic_SensorConfig_init_zero {false, meshtastic_SCD4X_config_init_zero, false, meshtastic_SEN5X_config_init_zero, false, meshtastic_SCD30_config_init_zero, false, meshtastic_SHTXX_config_init_zero}
|
||||
#define meshtastic_SensorConfig_init_zero {false, meshtastic_SCD4X_config_init_zero, false, meshtastic_SEN5X_config_init_zero, false, meshtastic_SCD30_config_init_zero, false, meshtastic_SHTXX_config_init_zero, false, meshtastic_DS248X_config_init_zero, false, meshtastic_SEN6X_config_init_zero}
|
||||
#define meshtastic_SCD4X_config_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||
#define meshtastic_SEN5X_config_init_zero {false, 0, false, 0}
|
||||
#define meshtastic_SEN5X_config_init_zero {false, 0, false, 0, false, 0}
|
||||
#define meshtastic_SEN6X_config_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||
#define meshtastic_SCD30_config_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||
#define meshtastic_SHTXX_config_init_zero {false, 0}
|
||||
#define meshtastic_DS248X_config_init_zero {false, 0}
|
||||
|
||||
/* Field tags (for use in manual encoding/decoding) */
|
||||
#define meshtastic_AdminMessage_InputEvent_event_code_tag 1
|
||||
@@ -608,6 +656,15 @@ extern "C" {
|
||||
#define meshtastic_SCD4X_config_set_power_mode_tag 7
|
||||
#define meshtastic_SEN5X_config_set_temperature_tag 1
|
||||
#define meshtastic_SEN5X_config_set_one_shot_mode_tag 2
|
||||
#define meshtastic_SEN5X_config_start_fan_cleaning_tag 3
|
||||
#define meshtastic_SEN6X_config_set_temperature_tag 1
|
||||
#define meshtastic_SEN6X_config_set_one_shot_mode_tag 2
|
||||
#define meshtastic_SEN6X_config_start_fan_cleaning_tag 3
|
||||
#define meshtastic_SEN6X_config_set_asc_tag 4
|
||||
#define meshtastic_SEN6X_config_set_target_co2_conc_tag 5
|
||||
#define meshtastic_SEN6X_config_set_altitude_tag 6
|
||||
#define meshtastic_SEN6X_config_set_ambient_pressure_tag 7
|
||||
#define meshtastic_SEN6X_config_factory_reset_tag 8
|
||||
#define meshtastic_SCD30_config_set_asc_tag 1
|
||||
#define meshtastic_SCD30_config_set_target_co2_conc_tag 2
|
||||
#define meshtastic_SCD30_config_set_temperature_tag 3
|
||||
@@ -615,10 +672,13 @@ extern "C" {
|
||||
#define meshtastic_SCD30_config_set_measurement_interval_tag 5
|
||||
#define meshtastic_SCD30_config_soft_reset_tag 6
|
||||
#define meshtastic_SHTXX_config_set_accuracy_tag 1
|
||||
#define meshtastic_DS248X_config_main_temperature_channel_tag 1
|
||||
#define meshtastic_SensorConfig_scd4x_config_tag 1
|
||||
#define meshtastic_SensorConfig_sen5x_config_tag 2
|
||||
#define meshtastic_SensorConfig_scd30_config_tag 3
|
||||
#define meshtastic_SensorConfig_shtxx_config_tag 4
|
||||
#define meshtastic_SensorConfig_ds248x_config_tag 5
|
||||
#define meshtastic_SensorConfig_sen6x_config_tag 6
|
||||
#define meshtastic_AdminMessage_get_channel_request_tag 1
|
||||
#define meshtastic_AdminMessage_get_channel_response_tag 2
|
||||
#define meshtastic_AdminMessage_get_owner_request_tag 3
|
||||
@@ -824,13 +884,17 @@ X(a, STATIC, OPTIONAL, UINT32, security_number, 4)
|
||||
X(a, STATIC, OPTIONAL, MESSAGE, scd4x_config, 1) \
|
||||
X(a, STATIC, OPTIONAL, MESSAGE, sen5x_config, 2) \
|
||||
X(a, STATIC, OPTIONAL, MESSAGE, scd30_config, 3) \
|
||||
X(a, STATIC, OPTIONAL, MESSAGE, shtxx_config, 4)
|
||||
X(a, STATIC, OPTIONAL, MESSAGE, shtxx_config, 4) \
|
||||
X(a, STATIC, OPTIONAL, MESSAGE, ds248x_config, 5) \
|
||||
X(a, STATIC, OPTIONAL, MESSAGE, sen6x_config, 6)
|
||||
#define meshtastic_SensorConfig_CALLBACK NULL
|
||||
#define meshtastic_SensorConfig_DEFAULT NULL
|
||||
#define meshtastic_SensorConfig_scd4x_config_MSGTYPE meshtastic_SCD4X_config
|
||||
#define meshtastic_SensorConfig_sen5x_config_MSGTYPE meshtastic_SEN5X_config
|
||||
#define meshtastic_SensorConfig_scd30_config_MSGTYPE meshtastic_SCD30_config
|
||||
#define meshtastic_SensorConfig_shtxx_config_MSGTYPE meshtastic_SHTXX_config
|
||||
#define meshtastic_SensorConfig_ds248x_config_MSGTYPE meshtastic_DS248X_config
|
||||
#define meshtastic_SensorConfig_sen6x_config_MSGTYPE meshtastic_SEN6X_config
|
||||
|
||||
#define meshtastic_SCD4X_config_FIELDLIST(X, a) \
|
||||
X(a, STATIC, OPTIONAL, BOOL, set_asc, 1) \
|
||||
@@ -845,10 +909,23 @@ X(a, STATIC, OPTIONAL, BOOL, set_power_mode, 7)
|
||||
|
||||
#define meshtastic_SEN5X_config_FIELDLIST(X, a) \
|
||||
X(a, STATIC, OPTIONAL, FLOAT, set_temperature, 1) \
|
||||
X(a, STATIC, OPTIONAL, BOOL, set_one_shot_mode, 2)
|
||||
X(a, STATIC, OPTIONAL, BOOL, set_one_shot_mode, 2) \
|
||||
X(a, STATIC, OPTIONAL, BOOL, start_fan_cleaning, 3)
|
||||
#define meshtastic_SEN5X_config_CALLBACK NULL
|
||||
#define meshtastic_SEN5X_config_DEFAULT NULL
|
||||
|
||||
#define meshtastic_SEN6X_config_FIELDLIST(X, a) \
|
||||
X(a, STATIC, OPTIONAL, FLOAT, set_temperature, 1) \
|
||||
X(a, STATIC, OPTIONAL, BOOL, set_one_shot_mode, 2) \
|
||||
X(a, STATIC, OPTIONAL, BOOL, start_fan_cleaning, 3) \
|
||||
X(a, STATIC, OPTIONAL, BOOL, set_asc, 4) \
|
||||
X(a, STATIC, OPTIONAL, UINT32, set_target_co2_conc, 5) \
|
||||
X(a, STATIC, OPTIONAL, UINT32, set_altitude, 6) \
|
||||
X(a, STATIC, OPTIONAL, UINT32, set_ambient_pressure, 7) \
|
||||
X(a, STATIC, OPTIONAL, BOOL, factory_reset, 8)
|
||||
#define meshtastic_SEN6X_config_CALLBACK NULL
|
||||
#define meshtastic_SEN6X_config_DEFAULT NULL
|
||||
|
||||
#define meshtastic_SCD30_config_FIELDLIST(X, a) \
|
||||
X(a, STATIC, OPTIONAL, BOOL, set_asc, 1) \
|
||||
X(a, STATIC, OPTIONAL, UINT32, set_target_co2_conc, 2) \
|
||||
@@ -864,6 +941,11 @@ X(a, STATIC, OPTIONAL, UINT32, set_accuracy, 1)
|
||||
#define meshtastic_SHTXX_config_CALLBACK NULL
|
||||
#define meshtastic_SHTXX_config_DEFAULT NULL
|
||||
|
||||
#define meshtastic_DS248X_config_FIELDLIST(X, a) \
|
||||
X(a, STATIC, OPTIONAL, UINT32, main_temperature_channel, 1)
|
||||
#define meshtastic_DS248X_config_CALLBACK NULL
|
||||
#define meshtastic_DS248X_config_DEFAULT NULL
|
||||
|
||||
extern const pb_msgdesc_t meshtastic_AdminMessage_msg;
|
||||
extern const pb_msgdesc_t meshtastic_AdminMessage_InputEvent_msg;
|
||||
extern const pb_msgdesc_t meshtastic_AdminMessage_OTAEvent_msg;
|
||||
@@ -875,8 +957,10 @@ extern const pb_msgdesc_t meshtastic_KeyVerificationAdmin_msg;
|
||||
extern const pb_msgdesc_t meshtastic_SensorConfig_msg;
|
||||
extern const pb_msgdesc_t meshtastic_SCD4X_config_msg;
|
||||
extern const pb_msgdesc_t meshtastic_SEN5X_config_msg;
|
||||
extern const pb_msgdesc_t meshtastic_SEN6X_config_msg;
|
||||
extern const pb_msgdesc_t meshtastic_SCD30_config_msg;
|
||||
extern const pb_msgdesc_t meshtastic_SHTXX_config_msg;
|
||||
extern const pb_msgdesc_t meshtastic_DS248X_config_msg;
|
||||
|
||||
/* Defines for backwards compatibility with code written before nanopb-0.4.0 */
|
||||
#define meshtastic_AdminMessage_fields &meshtastic_AdminMessage_msg
|
||||
@@ -890,23 +974,27 @@ extern const pb_msgdesc_t meshtastic_SHTXX_config_msg;
|
||||
#define meshtastic_SensorConfig_fields &meshtastic_SensorConfig_msg
|
||||
#define meshtastic_SCD4X_config_fields &meshtastic_SCD4X_config_msg
|
||||
#define meshtastic_SEN5X_config_fields &meshtastic_SEN5X_config_msg
|
||||
#define meshtastic_SEN6X_config_fields &meshtastic_SEN6X_config_msg
|
||||
#define meshtastic_SCD30_config_fields &meshtastic_SCD30_config_msg
|
||||
#define meshtastic_SHTXX_config_fields &meshtastic_SHTXX_config_msg
|
||||
#define meshtastic_DS248X_config_fields &meshtastic_DS248X_config_msg
|
||||
|
||||
/* Maximum encoded size of messages (where known) */
|
||||
#define MESHTASTIC_MESHTASTIC_ADMIN_PB_H_MAX_SIZE meshtastic_AdminMessage_size
|
||||
#define meshtastic_AdminMessage_InputEvent_size 14
|
||||
#define meshtastic_AdminMessage_OTAEvent_size 36
|
||||
#define meshtastic_AdminMessage_size 511
|
||||
#define meshtastic_DS248X_config_size 6
|
||||
#define meshtastic_HamParameters_size 47
|
||||
#define meshtastic_KeyVerificationAdmin_size 25
|
||||
#define meshtastic_LockdownAuth_size 56
|
||||
#define meshtastic_NodeRemoteHardwarePinsResponse_size 496
|
||||
#define meshtastic_SCD30_config_size 27
|
||||
#define meshtastic_SCD4X_config_size 29
|
||||
#define meshtastic_SEN5X_config_size 7
|
||||
#define meshtastic_SEN5X_config_size 9
|
||||
#define meshtastic_SEN6X_config_size 31
|
||||
#define meshtastic_SHTXX_config_size 6
|
||||
#define meshtastic_SensorConfig_size 77
|
||||
#define meshtastic_SensorConfig_size 120
|
||||
#define meshtastic_SharedContact_size 127
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -332,7 +332,7 @@ typedef enum _meshtastic_CotType {
|
||||
/* y-: TAKTALK room/membership broadcast. Payload carried via the
|
||||
TakTalkRoomData typed variant (sender_callsign, room_id, room_name,
|
||||
participants). The CoT type literally has a trailing dash and no
|
||||
second atom - not a typo. */
|
||||
second atom — not a typo. */
|
||||
meshtastic_CotType_CotType_y = 126
|
||||
} meshtastic_CotType;
|
||||
|
||||
@@ -380,7 +380,7 @@ typedef enum _meshtastic_DrawnShape_Kind {
|
||||
/* u-r-b-bullseye: Bullseye ring with range rings and bearing reference */
|
||||
meshtastic_DrawnShape_Kind_Kind_Bullseye = 7,
|
||||
/* u-d-c-e: Ellipse with distinct major/minor axes (same storage as
|
||||
Kind_Circle - uses major_cm/minor_cm/angle_deg - but receivers
|
||||
Kind_Circle — uses major_cm/minor_cm/angle_deg — but receivers
|
||||
render it as a non-circular ellipse rather than a round circle). */
|
||||
meshtastic_DrawnShape_Kind_Kind_Ellipse = 8,
|
||||
/* u-d-v: 2D vehicle outline drawn on the map. Vertices carry the
|
||||
@@ -400,7 +400,7 @@ typedef enum _meshtastic_DrawnShape_Kind {
|
||||
end of parse; builder uses it to decide which of <strokeColor> /
|
||||
<fillColor> to emit in the reconstructed XML. */
|
||||
typedef enum _meshtastic_DrawnShape_StyleMode {
|
||||
/* Unspecified - receiver infers from which color fields are non-zero. */
|
||||
/* Unspecified — receiver infers from which color fields are non-zero. */
|
||||
meshtastic_DrawnShape_StyleMode_StyleMode_Unspecified = 0,
|
||||
/* Stroke only. No <fillColor> in the source XML. Used for polylines,
|
||||
ranging lines, bullseye rings. */
|
||||
@@ -417,7 +417,7 @@ typedef enum _meshtastic_DrawnShape_StyleMode {
|
||||
alone is ambiguous (e.g. a-u-G could be a 2525 symbol or a custom icon
|
||||
depending on the iconset path). */
|
||||
typedef enum _meshtastic_Marker_Kind {
|
||||
/* Unspecified - fall back to TAKPacketV2.cot_type_id */
|
||||
/* Unspecified — fall back to TAKPacketV2.cot_type_id */
|
||||
meshtastic_Marker_Kind_Kind_Unspecified = 0,
|
||||
/* b-m-p-s-m: Spot map marker */
|
||||
meshtastic_Marker_Kind_Kind_Spot = 1,
|
||||
@@ -680,10 +680,10 @@ typedef struct _meshtastic_AircraftTrack {
|
||||
hundred meters of the anchor has per-vertex deltas in the ±10^4 range.
|
||||
Under sint32+zigzag those encode as 2 bytes each (tag+varint), versus the
|
||||
4 bytes that sfixed32 would always require. At 32 vertices that is ~128
|
||||
bytes of savings - the difference between fitting under the LoRa MTU or
|
||||
bytes of savings — the difference between fitting under the LoRa MTU or
|
||||
not. Absolute coordinates (values ~10^9) would cost sint32 varint 5 bytes
|
||||
per field, which is why TAKPacketV2's top-level latitude_i / longitude_i
|
||||
stay sfixed32 - only small values win with sint32. */
|
||||
stay sfixed32 — only small values win with sint32. */
|
||||
typedef struct _meshtastic_CotGeoPoint {
|
||||
/* Latitude delta from TAKPacketV2.latitude_i, in 1e-7 degree units.
|
||||
Add to the enclosing event's latitude_i to recover the absolute latitude. */
|
||||
@@ -791,7 +791,7 @@ typedef struct _meshtastic_Marker {
|
||||
|
||||
Covers CoT type u-rb-a. The anchor position is on
|
||||
TAKPacketV2.latitude_i/longitude_i; the target endpoint is carried as a
|
||||
CotGeoPoint - same delta-from-anchor encoding used by DrawnShape.vertices
|
||||
CotGeoPoint — same delta-from-anchor encoding used by DrawnShape.vertices
|
||||
so a self-anchored RAB (common case) encodes in zero bytes. */
|
||||
typedef struct _meshtastic_RangeAndBearing {
|
||||
/* Target/anchor endpoint (delta-encoded from TAKPacketV2.latitude_i/longitude_i). */
|
||||
@@ -899,12 +899,12 @@ typedef struct _meshtastic_CasevacReport {
|
||||
same as the envelope callsign but ATAK sometimes carries a distinct
|
||||
ops-number here. */
|
||||
pb_callback_t title;
|
||||
/* Primary medline free-text - the single most clinically important line
|
||||
/* Primary medline free-text — the single most clinically important line
|
||||
on a MEDLINE form (e.g. "2 urgent litter patients, smoke on approach").
|
||||
MUST be preserved under MTU pressure as long as any casevac is sent. */
|
||||
pb_callback_t medline_remarks;
|
||||
/* Line 3 (newer ATAK format): patient counts by precedence level.
|
||||
Coexists with the enum-style `precedence` field (tag 1) - older ATAK
|
||||
Coexists with the enum-style `precedence` field (tag 1) — older ATAK
|
||||
emits a single enum, newer ATAK emits these counts, and both can be
|
||||
set simultaneously. Senders populate whichever style(s) the source
|
||||
XML had; receivers prefer counts when non-zero. */
|
||||
@@ -946,19 +946,19 @@ typedef struct _meshtastic_CasevacReport {
|
||||
(e.g. "Primary HLZ is soccer field"). */
|
||||
pb_callback_t hlz_remarks;
|
||||
/* Per-patient clinical records. Each entry is one patient's ZMIST card
|
||||
(Zap number / Mechanism / Injuries / Signs / Treatment). Repeatable -
|
||||
(Zap number / Mechanism / Injuries / Signs / Treatment). Repeatable —
|
||||
a mass-casualty event can carry 1-6 entries in practice, limited by
|
||||
the 237 B LoRa MTU. */
|
||||
pb_callback_t zmist;
|
||||
} meshtastic_CasevacReport;
|
||||
|
||||
/* Per-patient clinical summary record - one entry per patient in a CASEVAC.
|
||||
/* Per-patient clinical summary record — one entry per patient in a CASEVAC.
|
||||
Maps directly to ATAK's <zMist> child element inside <zMistsMap>.
|
||||
All fields are optional free-text; senders populate what they have. */
|
||||
typedef struct _meshtastic_ZMistEntry {
|
||||
/* Patient identifier / sequence label (e.g. "ZMIST-1", "ZMIST-2"). */
|
||||
pb_callback_t title;
|
||||
/* Zap number - unique patient tracking ID (often a terse code like
|
||||
/* Zap number — unique patient tracking ID (often a terse code like
|
||||
"Gunshot" or a serial). */
|
||||
pb_callback_t z;
|
||||
/* Mechanism of injury (e.g. "Penetrating trauma", "Blast injury"). */
|
||||
@@ -997,7 +997,7 @@ typedef struct _meshtastic_EmergencyAlert {
|
||||
creation time; the fields below carry structured metadata the raw-detail
|
||||
fallback currently loses.
|
||||
|
||||
Fields are deliberately lean - this variant is closer to the MTU ceiling
|
||||
Fields are deliberately lean — this variant is closer to the MTU ceiling
|
||||
than the others, so every string is capped in options. */
|
||||
typedef struct _meshtastic_TaskRequest {
|
||||
/* Short tag for the task category (e.g. "engage", "observe", "recon",
|
||||
@@ -1017,7 +1017,7 @@ typedef struct _meshtastic_TaskRequest {
|
||||
|
||||
/* Weather annotation from <environment> CoT detail element.
|
||||
|
||||
Attaches to any TAKPacketV2 regardless of payload_variant - an Aircraft,
|
||||
Attaches to any TAKPacketV2 regardless of payload_variant — an Aircraft,
|
||||
PLI, or Marker can all carry observed conditions at the emitting station.
|
||||
ATAK-CIV ships an XSD for <environment> but no dedicated handler, so the
|
||||
element round-trips through the generic detail pipeline; this message
|
||||
@@ -1026,7 +1026,7 @@ typedef struct _meshtastic_TaskRequest {
|
||||
Target wire cost: ~6-8 bytes compressed with a fully populated instance.
|
||||
|
||||
Named `TAKEnvironment` (not just `Environment`) because the bare name
|
||||
collides with `SwiftUI.Environment` - every SwiftUI view in a consuming
|
||||
collides with `SwiftUI.Environment` — every SwiftUI view in a consuming
|
||||
iOS app uses the `@Environment` property wrapper, and importing the
|
||||
generated proto module would make `Environment` ambiguous in every one
|
||||
of those files. The `TAK` prefix matches the convention used by the
|
||||
@@ -1055,7 +1055,7 @@ typedef struct _meshtastic_TAKEnvironment {
|
||||
The receiving ATAK client restores those from its own defaults, same as
|
||||
every other CoT carried over Meshtastic today.
|
||||
|
||||
Attaches to any TAKPacketV2 - a PLI with a sensor on the operator's head,
|
||||
Attaches to any TAKPacketV2 — a PLI with a sensor on the operator's head,
|
||||
an Aircraft with a FLIR turret, a Marker dropped on a UAV.
|
||||
Target wire cost: ~7-14 bytes compressed (dominated by model string). */
|
||||
typedef struct _meshtastic_SensorFov {
|
||||
@@ -1065,30 +1065,30 @@ typedef struct _meshtastic_SensorFov {
|
||||
SensorDetailHandler default (270°) and save varint bytes over centi-deg. */
|
||||
uint32_t azimuth_deg;
|
||||
/* Maximum range of the cone in meters.
|
||||
Optional - if unset, receivers should use the ATAK-CIV default of 100m. */
|
||||
Optional — if unset, receivers should use the ATAK-CIV default of 100m. */
|
||||
bool has_range_m;
|
||||
uint32_t range_m;
|
||||
/* Horizontal field of view in whole degrees (cone's angular width).
|
||||
ATAK-CIV default is 45°. */
|
||||
uint32_t fov_horizontal_deg;
|
||||
/* Vertical field of view in whole degrees. ATAK-CIV default is 45°.
|
||||
Optional - a value of 0 means "not set / use horizontal FOV". */
|
||||
Optional — a value of 0 means "not set / use horizontal FOV". */
|
||||
uint32_t fov_vertical_deg;
|
||||
/* Elevation angle in whole degrees. Positive = up, negative = down.
|
||||
Range -90 to +90. sint32 for varint efficiency on small negatives. */
|
||||
int32_t elevation_deg;
|
||||
/* Roll (camera tilt) in whole degrees, -180 to +180.
|
||||
Optional - use 0 if the sensor doesn't track roll. */
|
||||
Optional — use 0 if the sensor doesn't track roll. */
|
||||
int32_t roll_deg;
|
||||
/* Free-form device model identifier, e.g. "FLIR-Boson-640", "SEEK".
|
||||
Optional - empty string means "unknown model" (ATAK-CIV default). */
|
||||
Optional — empty string means "unknown model" (ATAK-CIV default). */
|
||||
pb_callback_t model;
|
||||
} meshtastic_SensorFov;
|
||||
|
||||
/* TAKTALK chat message payload (CoT type m-t-t).
|
||||
|
||||
TAKTALK is an ATAK plugin for voice + text team messaging. The voice
|
||||
audio stream goes over UDP/RTP and is NOT carried by the mesh - only
|
||||
audio stream goes over UDP/RTP and is NOT carried by the mesh — only
|
||||
the text envelope (this message) is. `from_voice` marks messages sent
|
||||
via push-to-talk speech-to-text so receivers can render a mic icon
|
||||
next to the text.
|
||||
@@ -1122,7 +1122,7 @@ typedef struct _meshtastic_TakTalkMessage {
|
||||
Announces a TAKTALK chatroom's friendly name and roster so peers can
|
||||
resolve room UUIDs (used in TakTalkMessage.chatroom_id and
|
||||
GeoChat.room_id) to a display name and participant list. Not a chat
|
||||
message itself - these events are emitted by TAKTALK when rooms are
|
||||
message itself — these events are emitted by TAKTALK when rooms are
|
||||
created or memberships change. */
|
||||
typedef struct _meshtastic_TakTalkRoomData {
|
||||
/* Callsign of the device broadcasting the room state (typically the
|
||||
@@ -1161,7 +1161,7 @@ typedef struct _meshtastic_Marti {
|
||||
primary-vs-cc distinction the same way ATAK does.
|
||||
|
||||
If dest_callsign is [TAKPacketV2.callsign] (self-addressed, unusual but
|
||||
legal - e.g. ATAK echoing back to its own room), the builder still emits
|
||||
legal — e.g. ATAK echoing back to its own room), the builder still emits
|
||||
the element so loopback shapes round-trip cleanly. */
|
||||
pb_callback_t dest_callsign;
|
||||
} meshtastic_Marti;
|
||||
|
||||
@@ -24,7 +24,7 @@ PB_BIND(meshtastic_NodePositionEntry, meshtastic_NodePositionEntry, AUTO)
|
||||
PB_BIND(meshtastic_NodeTelemetryEntry, meshtastic_NodeTelemetryEntry, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_NodeEnvironmentEntry, meshtastic_NodeEnvironmentEntry, AUTO)
|
||||
PB_BIND(meshtastic_NodeEnvironmentEntry, meshtastic_NodeEnvironmentEntry, 2)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_NodeStatusEntry, meshtastic_NodeStatusEntry, AUTO)
|
||||
|
||||
@@ -458,7 +458,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg;
|
||||
#define meshtastic_BackupPreferences_size 2740
|
||||
#define meshtastic_ChannelFile_size 718
|
||||
#define meshtastic_DeviceState_size 1944
|
||||
#define meshtastic_NodeEnvironmentEntry_size 170
|
||||
#define meshtastic_NodeEnvironmentEntry_size 218
|
||||
#define meshtastic_NodeInfoLite_size 112
|
||||
#define meshtastic_NodePositionEntry_size 42
|
||||
#define meshtastic_NodeStatusEntry_size 89
|
||||
|
||||
@@ -1112,10 +1112,8 @@ typedef struct _meshtastic_MeshPacket {
|
||||
meshtastic_MeshPacket_Priority priority;
|
||||
/* 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. */
|
||||
0 dBm; SX127x's formula can even go positive). has_rx_rssi disambiguates; a replayed packet
|
||||
built from history should leave this field absent rather than emitting 0. */
|
||||
bool has_rx_rssi;
|
||||
int32_t rx_rssi;
|
||||
/* Describe if this message is delayed */
|
||||
@@ -1269,15 +1267,15 @@ typedef struct _meshtastic_LockdownStatus {
|
||||
/* Current lockdown state being reported. */
|
||||
meshtastic_LockdownStatus_State state;
|
||||
/* For LOCKED: machine-readable reason. Known values:
|
||||
"needs_auth" - storage already unlocked, client must auth
|
||||
"token_missing" - no boot token on flash
|
||||
"token_expired" - boot token wall-clock TTL elapsed
|
||||
"token_boots_zero" - boot token boot-count TTL exhausted
|
||||
"token_hmac_fail" - token tampered or wrong device
|
||||
"token_dek_fail" - token DEK decrypt failed
|
||||
"token_wrong_size" - token file corrupted
|
||||
"token_bad_magic" - token file corrupted
|
||||
"not_provisioned" - should generally use NEEDS_PROVISION state instead
|
||||
"needs_auth" — storage already unlocked, client must auth
|
||||
"token_missing" — no boot token on flash
|
||||
"token_expired" — boot token wall-clock TTL elapsed
|
||||
"token_boots_zero" — boot token boot-count TTL exhausted
|
||||
"token_hmac_fail" — token tampered or wrong device
|
||||
"token_dek_fail" — token DEK decrypt failed
|
||||
"token_wrong_size" — token file corrupted
|
||||
"token_bad_magic" — token file corrupted
|
||||
"not_provisioned" — should generally use NEEDS_PROVISION state instead
|
||||
Other values may be added; clients should treat unknown values as
|
||||
"locked, ask for passphrase". */
|
||||
char lock_reason[32];
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
/* Payload for MESH_BEACON_APP packets.
|
||||
Periodically broadcast by nodes in beacon mode.
|
||||
Listeners deliver the text message to the local inbox and cache any offered
|
||||
channel/preset for the client app to act on - the firmware never auto-applies them. */
|
||||
channel/preset for the client app to act on — the firmware never auto-applies them. */
|
||||
typedef struct _meshtastic_MeshBeacon {
|
||||
/* Human-readable beacon message. Max 100 bytes enforced by firmware on send. */
|
||||
char message[101];
|
||||
|
||||
Loaded 100 of 208 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user