Merge develop into packet authentication policy
This commit is contained in:
@@ -763,7 +763,7 @@ The repo registers the server via `.mcp.json` at the repo root - Claude Code / C
|
||||
|
||||
**One MCP call per port at a time.** `SerialInterface` holds an exclusive OS-level lock on the serial port for its lifetime. If a `serial_*` session is open on `/dev/cu.usbmodem101`, calling `device_info` on the same port will fail fast pointing at the active session. Sequence calls: open → read/mutate → close, then next device. Never parallelize tool calls on the same port.
|
||||
|
||||
### MCP tool surface (43 tools)
|
||||
### MCP tool surface (44 tools)
|
||||
|
||||
Grouped by purpose. Full argument shapes in the meshtastic-mcp repo's README; a few high-value signatures are called out here.
|
||||
|
||||
@@ -771,7 +771,7 @@ Grouped by purpose. Full argument shapes in the meshtastic-mcp repo's README; a
|
||||
- **Build & flash**: `build`, `clean`, `pio_flash`, `erase_and_flash` (ESP32 only), `update_flash` (ESP32 OTA), `touch_1200bps`
|
||||
- **Serial sessions** (long-running, 10k-line ring buffer): `serial_open`, `serial_read`, `serial_list`, `serial_close`
|
||||
- **Device reads**: `device_info`, `list_nodes`
|
||||
- **Device writes**: `set_owner`, `get_config`, `set_config`, `get_channel_url`, `set_channel_url`, `send_text`, `send_input_event` (inject a button/key press via the firmware's InputBroker), `set_debug_log_api`; destructive/power-state writes require `confirm=True`: `reboot`, `shutdown`, `factory_reset`
|
||||
- **Device writes**: `set_owner`, `get_config`, `set_config`, `get_channel_url`, `set_channel_url`, `send_text`, `send_input_event` (inject a button/key press via the firmware's InputBroker), `inject_frame` (inject an over-the-air-style frame into the RX pipeline - see below), `set_debug_log_api`; destructive/power-state writes require `confirm=True`: `reboot`, `shutdown`, `factory_reset`
|
||||
- **userPrefs admin** (build-time constants, not runtime config): `userprefs_get`, `userprefs_set`, `userprefs_reset`, `userprefs_manifest`, `userprefs_testing_profile`
|
||||
- **Vendor escape hatches**: `esptool_chip_info`, `esptool_erase_flash`, `esptool_raw`, `nrfutil_dfu`, `nrfutil_raw`, `picotool_info`, `picotool_load`, `picotool_raw`
|
||||
- **USB power control** (via `uhubctl`, per-port PPPS toggle): `uhubctl_list` (read-only), `uhubctl_power(action='on'|'off', confirm=True)`, `uhubctl_cycle(delay_s, confirm=True)`. Target by raw `(location, port)` or by `role` (`"nrf52"`, `"esp32s3"`); role lookup checks `MESHTASTIC_UHUBCTL_LOCATION_<ROLE>` + `_PORT_<ROLE>` env vars first, falls back to VID auto-detection.
|
||||
@@ -781,6 +781,15 @@ Grouped by purpose. Full argument shapes in the meshtastic-mcp repo's README; a
|
||||
|
||||
**TCP / native-host nodes.** Setting `MESHTASTIC_MCP_TCP_HOST=<host[:port]>` makes `list_devices` surface a `meshtasticd` daemon (e.g. the `native-macos` build) as a synthetic `tcp://host:port` entry, and `connect()` routes through `meshtastic.tcp_interface.TCPInterface` instead of `SerialInterface`. Every read/write/admin tool that flows through `connect()` works against the daemon transparently. USB-only tools (`pio_flash`, `erase_and_flash`, `update_flash`, `touch_1200bps`, `serial_open`, `esptool_*`, `nrfutil_*`, `picotool_*`) raise a clear `ConnectionError` when handed a `tcp://` port; `pio_flash` against a `native*` env raises a `FlashError` (no upload step - use `build` and run the binary directly). The pytest harness still assumes USB-attached devices per role; TCP-aware fixtures are deferred. See the meshtastic-mcp repo's README § "TCP / native-host nodes".
|
||||
|
||||
### Frame injection: testing the off-air receive path
|
||||
|
||||
The `toRadio` API can only inject **locally-originated** traffic - the firmware forces `p.from = 0` in `MeshService::handleToRadio`, which bypasses the `from != 0` receive path and everything gated on it (remote admin authorization, the admin session-passkey check, hop handling, promiscuous sniffing). To exercise those paths you either need a second transmitting radio, or **frame injection**: a build-flagged seam that delivers a client-supplied frame into the real RX pipeline as if it arrived off the LoRa chip.
|
||||
|
||||
- **Firmware:** build with `-D MESHTASTIC_ENABLE_FRAME_INJECTION=1` (`src/configuration.h`, off by default - it forges over-the-air traffic and must never ship enabled). `MeshService::injectAsReceived` extends the existing portduino `SimRadio` `SIMULATOR_APP` path to real hardware: it unwraps a `Compressed` envelope (`portnum == UNKNOWN_APP` → verbatim ciphertext the router decrypts; else → decoded payload for that portnum), then calls `router->enqueueReceivedMessage()` - the exact entry point `RadioLibInterface::handleReceiveInterrupt` uses. Injection is reached before the `p.from = 0` line, so a forged sender survives; `from == 0` is dropped to match real RX.
|
||||
- **Host:** drive it with the meshtastic-mcp `inject_frame` tool (or `cli/meshinject.py`). The crafter replicates channel crypto (default-PSK expansion, `xorHash` channel hash, AES-CTR with the `packetId|from|0` nonce), so an encrypted frame decrypts on-device as if received. Modes: `text`, `raw`, `admin` (+ `pki`/`public_key` for the PKC-admin path), `ciphertext`, `fuzz` (malformed-frame decode-path/crash testing).
|
||||
- **Example - remote-admin session-key repro:** set the target's `admin_key[0]` to a key you hold, then inject an `admin` set_owner with `pki=true`, that key, and a stale `session_hex`. The node logs `PKC admin payload with authorized sender key` → `Expected session key: 00…` → `Admin message without session_key!` - the exact ndoo scenario, on real silicon. Capture logs via `set_debug_log_api` on the same connection.
|
||||
- **nRF52 gotcha:** the USB CDC wedges under rapid `SerialInterface` open/close churn (unrelated to injection) - keep setup + inject + log-capture on one connection; recover a hung board via a 1200 bps-touch DFU reflash.
|
||||
|
||||
### Hardware test suite (`run-tests.sh`, from a meshtastic-mcp checkout)
|
||||
|
||||
The wrapper auto-detects connected devices (VID → role map: `0x239A` → `nrf52`, `0x303A`/`0x10C4` → `esp32s3`), maps each role to a PlatformIO env (`nrf52` → `rak4631`, `esp32s3` → `heltec-v3`, overridable via `MESHTASTIC_MCP_ENV_<ROLE>`), then invokes pytest. Zero pre-flight config needed from the operator.
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
name: Build Windows Binary
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
windows_ver:
|
||||
required: false
|
||||
default: "2025"
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-Windows:
|
||||
runs-on: windows-${{ inputs.windows_ver }}
|
||||
defaults:
|
||||
run:
|
||||
# UCRT64 is the MinGW-w64 environment native-windows targets; the `msys`
|
||||
# environment would link msys-2.0.dll and produce a Cygwin-style binary.
|
||||
shell: msys2 {0}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
# Keep the token out of .git/config so later steps and artifacts can't leak it.
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup MSYS2 / UCRT64
|
||||
id: msys2
|
||||
uses: msys2/setup-msys2@v2
|
||||
with:
|
||||
msystem: UCRT64
|
||||
update: true
|
||||
# argp is not packaged for the mingw environments and is built below.
|
||||
# Python is omitted too: MSYS2's reports a `mingw` platform tag no wheel matches.
|
||||
install: >-
|
||||
mingw-w64-ucrt-x86_64-gcc
|
||||
mingw-w64-ucrt-x86_64-pkgconf
|
||||
mingw-w64-ucrt-x86_64-yaml-cpp
|
||||
mingw-w64-ucrt-x86_64-libuv
|
||||
mingw-w64-ucrt-x86_64-jsoncpp
|
||||
mingw-w64-ucrt-x86_64-openssl
|
||||
mingw-w64-ucrt-x86_64-libusb
|
||||
mingw-w64-ucrt-x86_64-cmake
|
||||
mingw-w64-ucrt-x86_64-ninja
|
||||
git
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
# framework-portduino calls argp_parse(); MSYS2 packages argp only for the
|
||||
# msys runtime, which cannot link into a native binary. No install() rules.
|
||||
- name: Build and install argp-standalone
|
||||
run: |
|
||||
git clone --depth 1 https://github.com/tom42/argp-standalone /tmp/argp
|
||||
cd /tmp/argp
|
||||
cmake -G Ninja -B build -DCMAKE_BUILD_TYPE=Release .
|
||||
cmake --build build
|
||||
cp include/argp-standalone/argp.h /ucrt64/include/argp.h
|
||||
cp build/src/libargp-standalone.a /ucrt64/lib/libargp.a
|
||||
|
||||
- name: Install PlatformIO
|
||||
shell: pwsh
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install platformio
|
||||
|
||||
- name: Get release version string
|
||||
shell: pwsh
|
||||
id: version
|
||||
run: echo "long=$(python ./bin/buildinfo.py long)" >> $env:GITHUB_OUTPUT
|
||||
|
||||
# Runs outside the MSYS2 shell so PlatformIO stays on the runner's
|
||||
# CPython; the UCRT64 toolchain is reached through PATH instead.
|
||||
- name: Build for Windows
|
||||
shell: pwsh
|
||||
run: |
|
||||
$env:PATH = "${{ steps.msys2.outputs.msys2-location }}\ucrt64\bin;$env:PATH"
|
||||
platformio run -e native-windows
|
||||
env:
|
||||
PKG_VERSION: ${{ steps.version.outputs.long }}
|
||||
|
||||
- name: List output files
|
||||
run: ls -lah .pio/build/native-windows/
|
||||
|
||||
# The env links statically, so only Windows system DLLs should appear here.
|
||||
# A third-party DLL means the static link regressed.
|
||||
- name: Verify the binary is self-contained
|
||||
run: |
|
||||
set -euo pipefail
|
||||
bin=.pio/build/native-windows/meshtasticd.exe
|
||||
test -f "$bin"
|
||||
# `|| true` keeps grep's no-match exit out of `set -e`; an empty import
|
||||
# table is caught by the test below instead of passing as "no deps".
|
||||
deps=$(objdump -p "$bin" | grep -i 'DLL Name' || true)
|
||||
test -n "$deps"
|
||||
extra=$(printf '%s\n' "$deps" \
|
||||
| grep -viE 'api-ms-win|KERNEL32|WS2_32|ADVAPI32|USER32|msvcrt|ucrtbase|bcrypt|IPHLPAPI|SHELL32|ole32|CRYPT32|SETUPAPI|CFGMGR32|WINMM|dbghelp' || true)
|
||||
if [ -n "$extra" ]; then
|
||||
printf '%s\n' "$extra"
|
||||
echo "::error::meshtasticd.exe has non-system DLL dependencies (static link regressed)"
|
||||
exit 1
|
||||
fi
|
||||
echo "OK: no third-party DLL dependencies"
|
||||
|
||||
- name: Smoke test the binary
|
||||
run: |
|
||||
.pio/build/native-windows/meshtasticd.exe --version
|
||||
|
||||
- name: Store binaries as an artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: firmware-windows-${{ inputs.windows_ver }}-${{ steps.version.outputs.long }}
|
||||
overwrite: true
|
||||
path: |
|
||||
.pio/build/native-windows/meshtasticd.exe
|
||||
@@ -144,6 +144,18 @@ jobs:
|
||||
macos_ver: ${{ matrix.macos_ver }}
|
||||
# secrets: inherit
|
||||
|
||||
Windows:
|
||||
if: ${{ github.event_name != 'schedule' && github.event.inputs.nightly != 'true' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
windows_ver:
|
||||
- "2025" # x86_64
|
||||
uses: ./.github/workflows/build_windows_bin.yml
|
||||
with:
|
||||
windows_ver: ${{ matrix.windows_ver }}
|
||||
# secrets: inherit
|
||||
|
||||
package-pio-deps-native-tft:
|
||||
if: ${{ github.repository == 'meshtastic/firmware' && github.event_name == 'workflow_dispatch' }}
|
||||
uses: ./.github/workflows/package_pio_deps.yml
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Stale PR+Issues
|
||||
uses: actions/stale@v10.3.0
|
||||
uses: actions/stale@v10.4.0
|
||||
with:
|
||||
days-before-stale: 45
|
||||
stale-issue-message: This issue has not had any comment or update in the last month. If it is still relevant, please post update comments. If no comments are made, this issue will be closed automagically in 7 days.
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
pio upgrade
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.pio
|
||||
pio
|
||||
.pio-docker
|
||||
pio.tar
|
||||
web
|
||||
web.tar
|
||||
|
||||
@@ -50,6 +50,7 @@ The [meshtastic-mcp](https://github.com/meshtastic/meshtastic-mcp) server expose
|
||||
- **Serial sessions**: `serial_open`, `serial_read`, `serial_list`, `serial_close`
|
||||
- **Device reads**: `device_info`, `list_nodes`
|
||||
- **Device writes** (require `confirm=True`): `set_owner`, `get_config`, `set_config`, `get_channel_url`, `set_channel_url`, `send_text`, `reboot`, `shutdown`, `factory_reset`, `set_debug_log_api`
|
||||
- **Frame injection**: `inject_frame` - deliver a crafted frame into a board's real RX pipeline as if received off LoRa (reaches `from != 0` / decrypt / remote-admin paths the `toRadio` API can't). Needs firmware built with `-D MESHTASTIC_ENABLE_FRAME_INJECTION=1` (`MeshService::injectAsReceived`); sim nodes always. See the copilot-instructions **Frame injection** section.
|
||||
- **userPrefs admin**: `userprefs_get`, `userprefs_set`, `userprefs_reset`, `userprefs_manifest`, `userprefs_testing_profile`
|
||||
- **Vendor escape hatches**: `esptool_*`, `nrfutil_*`, `picotool_*`
|
||||
|
||||
|
||||
+34
-3
@@ -153,6 +153,30 @@ Display:
|
||||
### You can also specify the spi device for the display to use
|
||||
# spidev: spidev0.0
|
||||
|
||||
### HUB75 RGB LED matrix panel (Raspberry Pi only, via hzeller/rpi-rgb-led-matrix).
|
||||
### Requires the librgbmatrix library to be installed (so `pkg-config rgbmatrix` resolves) and
|
||||
### the firmware built with it present. meshtasticd must run as root (or with /dev/gpiomem
|
||||
### access) and on-board audio disabled (dtparam=audio=off) so it doesn't fight the PWM timing.
|
||||
### The library owns the GPIO pins - they are chosen by HardwareMapping, not set individually.
|
||||
# Panel: HUB75
|
||||
# HUB75:
|
||||
# HardwareMapping: adafruit-hat-pwm # regular | adafruit-hat | adafruit-hat-pwm | regular-pi1 ...
|
||||
# Rows: 64 # pixel rows per panel (e.g. 32 or 64)
|
||||
# Cols: 64 # pixel columns per panel
|
||||
# ChainLength: 1 # panels daisy-chained (width = Cols * ChainLength)
|
||||
# Parallel: 1 # parallel chains (height = Rows * Parallel)
|
||||
# Brightness: 80 # percent, 1..100
|
||||
# PWMBits: 11 # lower = higher refresh, fewer colors
|
||||
# GPIOSlowdown: 4 # raise for faster Pis / long cables if the panel ghosts or flickers
|
||||
# RGBSequence: RGB # reorder if your panel's colors are swapped (e.g. RBG, BGR)
|
||||
# ScanMode: 0 # 0=progressive, 1=interlaced
|
||||
# RowAddressType: 0 # 0=default, 1=AB-addressed (some 64x64 panels)
|
||||
# Multiplexing: 0 # 0=direct, 1=stripe, 2=checker, ...
|
||||
# PanelType: "" # e.g. FM6126A for panels needing a special init
|
||||
# PixelMapper: "" # e.g. "U-mapper;Rotate:90"
|
||||
# LimitRefreshRateHz: 0 # 0 = no limit
|
||||
# DisableHardwarePulsing: false
|
||||
|
||||
Touchscreen:
|
||||
### Note, at least for now, the touchscreen must have a CS pin defined, even if you let Linux manage the CS switching.
|
||||
|
||||
@@ -167,12 +191,21 @@ Touchscreen:
|
||||
### You can also specify the spi device for the touchscreen to use
|
||||
# spidev: spidev0.0
|
||||
|
||||
|
||||
Input:
|
||||
### Configure device for direct keyboard input
|
||||
|
||||
# KeyboardDevice: /dev/input/by-id/usb-_Raspberry_Pi_Internal_Keyboard-event-kbd
|
||||
|
||||
### Configure device for USB gamepad/joystick input (evdev). The D-pad drives
|
||||
### Up/Down/Left/Right; buttons are mapped below. Find button codes with `evtest`.
|
||||
# JoystickDevice: /dev/input/by-id/usb-0079_USB_Gamepad-event-joystick
|
||||
### Map evdev button codes (hex or decimal) to actions. Omit to use built-in
|
||||
### defaults (select=0x121, cancel=0x122). Actions: select, cancel, back, up,
|
||||
### down, left, right, user.
|
||||
# JoystickButtons:
|
||||
# select: 0x122
|
||||
# cancel: 0x121
|
||||
|
||||
### Standard User Button Config
|
||||
# UserButton: 6
|
||||
|
||||
@@ -199,13 +232,11 @@ Webserver:
|
||||
# SSLKey: /etc/meshtasticd/ssl/private_key.pem # Path to SSL Key, generated if not present
|
||||
# SSLCert: /etc/meshtasticd/ssl/certificate.pem # Path to SSL Certificate, generated if not present
|
||||
|
||||
|
||||
HostMetrics:
|
||||
# ReportInterval: 30 # Interval in minutes between HostMetrics report packets, or 0 for disabled
|
||||
# Channel: 0 # channel to send Host Metrics over. Defaults to the primary channel.
|
||||
# UserStringCommand: cat /sys/firmware/devicetree/base/serial-number # Command to execute, to send the results as the userString
|
||||
|
||||
|
||||
Config:
|
||||
# DisplayMode: TWOCOLOR # uncomment to force BaseUI
|
||||
# DisplayMode: COLOR # uncomment to force MUI
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
Meta:
|
||||
name: HUB75 RGB LED Matrix (64x64)
|
||||
support: community
|
||||
compatible:
|
||||
- raspberry-pi
|
||||
|
||||
### HUB75 RGB LED matrix panel driven by hzeller/rpi-rgb-led-matrix.
|
||||
###
|
||||
### Prerequisites:
|
||||
### - Install the rpi-rgb-led-matrix library so `pkg-config rgbmatrix` resolves, and build the
|
||||
### firmware with it present (the HUB75 backend is compiled in automatically when found).
|
||||
### - meshtasticd must run as root (or have /dev/gpiomem access) for the library's direct GPIO.
|
||||
### - Disable on-board audio (dtparam=audio=off in /boot/firmware/config.txt) - it shares the
|
||||
### PWM hardware and causes flicker otherwise.
|
||||
###
|
||||
### The library owns the GPIO pins; they are selected by HardwareMapping (matching your HAT /
|
||||
### wiring), not assigned individually.
|
||||
Display:
|
||||
Panel: HUB75
|
||||
HUB75:
|
||||
HardwareMapping: adafruit-hat-pwm
|
||||
Rows: 64
|
||||
Cols: 64
|
||||
ChainLength: 1
|
||||
Parallel: 1
|
||||
Brightness: 80
|
||||
PWMBits: 11
|
||||
GPIOSlowdown: 4
|
||||
RGBSequence: RGB
|
||||
# Uncomment/tune as needed for your specific panel:
|
||||
# ScanMode: 0
|
||||
# RowAddressType: 0
|
||||
# Multiplexing: 0
|
||||
# PanelType: FM6126A
|
||||
# PixelMapper: ""
|
||||
# LimitRefreshRateHz: 0
|
||||
# DisableHardwarePulsing: false
|
||||
@@ -0,0 +1,20 @@
|
||||
#copy of lora-meshstick-1262.yaml data changed to match FrameTastic 1262 board
|
||||
|
||||
Meta:
|
||||
name: FrameTastic 1262
|
||||
support: community
|
||||
compatible:
|
||||
- usb
|
||||
|
||||
|
||||
Lora:
|
||||
Module: sx1262
|
||||
CS: 0
|
||||
IRQ: 6
|
||||
Reset: 2
|
||||
Busy: 4
|
||||
spidev: ch341
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
# USB_Serialnum: 12345678
|
||||
USB_PID: 0x5512
|
||||
USB_VID: 0x1A86
|
||||
@@ -0,0 +1,20 @@
|
||||
#Board uses wehoopers zebra hat config # https://github.com/wehooper4/Meshtastic-Hardware/tree/main/ZebraHAT
|
||||
|
||||
Meta:
|
||||
name: PiTastic 1W
|
||||
support: community
|
||||
compatible:
|
||||
- raspberry-pi
|
||||
|
||||
Lora:
|
||||
Module: sx1262
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
SX126X_MAX_POWER: 18
|
||||
CS: 24
|
||||
IRQ: 22
|
||||
Busy: 27
|
||||
Reset: 17
|
||||
|
||||
I2C:
|
||||
I2CDevice: /dev/i2c-1
|
||||
@@ -0,0 +1,21 @@
|
||||
# PiMesh-1W (V1) E22-900M30S
|
||||
# https://meshsmith.net/products/pimesh-1w
|
||||
Meta:
|
||||
name: PiMesh-1W (V1) E22-900M30S
|
||||
support: community
|
||||
compatible:
|
||||
- raspberry-pi
|
||||
|
||||
Lora:
|
||||
Module: sx1262
|
||||
CS: 21
|
||||
IRQ: 16
|
||||
Reset: 18
|
||||
Busy: 20
|
||||
TXen: 13
|
||||
RXen: 12
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
SX126X_MAX_POWER: 22
|
||||
spidev: spidev0.0
|
||||
|
||||
# preamble_length: 17
|
||||
@@ -0,0 +1,21 @@
|
||||
# PiMesh-1W (V2) E22P-915M30S / E22P-868M30S
|
||||
# https://meshsmith.net/products/pimesh-1w
|
||||
Meta:
|
||||
name: PiMesh-1W (V2) E22P-915M30S / E22P-868M30S
|
||||
support: community
|
||||
compatible:
|
||||
- raspberry-pi
|
||||
|
||||
Lora:
|
||||
Module: sx1262
|
||||
IRQ: 6
|
||||
Reset: 18
|
||||
Busy: 5
|
||||
CS: 8
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
SX126X_MAX_POWER: 22
|
||||
spidev: spidev0.0
|
||||
|
||||
# add the following to /boot/firmware/config.txt
|
||||
# gpio=26=op,dh
|
||||
+15
-8
@@ -7,9 +7,9 @@ MCU=""
|
||||
# Constants
|
||||
RESET_BAUD=1200
|
||||
FIRMWARE_OFFSET=0x00
|
||||
# Default littlefs* offset.
|
||||
OFFSET=0x300000
|
||||
# Default OTA Offset
|
||||
# Fallback offsets, used only when firmware metadata carries no partition table.
|
||||
# These track partition-table.csv; boards with their own table override them below.
|
||||
SPIFFS_OFFSET=0x300000
|
||||
OTA_OFFSET=0x260000
|
||||
|
||||
# Determine the correct esptool command to use
|
||||
@@ -122,8 +122,15 @@ if [[ -f "$FILENAME" && "$FILENAME" == *.factory.bin ]]; then
|
||||
jq . "$METAFILE"
|
||||
# Extract relevant fields from metadata
|
||||
if [[ $(jq -r '.part' "$METAFILE") != "null" ]]; then
|
||||
OTA_OFFSET=$(jq -r '.part[] | select(.subtype == "ota_1") | .offset' "$METAFILE")
|
||||
SPIFFS_OFFSET=$(jq -r '.part[] | select(.subtype == "spiffs") | .offset' "$METAFILE")
|
||||
META_OTA_OFFSET=$(jq -r '.part[] | select(.subtype == "ota_1") | .offset' "$METAFILE")
|
||||
META_SPIFFS_OFFSET=$(jq -r '.part[] | select(.subtype == "spiffs") | .offset' "$METAFILE")
|
||||
# A partition table may omit either entry; keep the built-in default then.
|
||||
if [[ -n "$META_OTA_OFFSET" && "$META_OTA_OFFSET" != "null" ]]; then
|
||||
OTA_OFFSET="$META_OTA_OFFSET"
|
||||
fi
|
||||
if [[ -n "$META_SPIFFS_OFFSET" && "$META_SPIFFS_OFFSET" != "null" ]]; then
|
||||
SPIFFS_OFFSET="$META_SPIFFS_OFFSET"
|
||||
fi
|
||||
fi
|
||||
MCU=$(jq -r '.mcu' "$METAFILE")
|
||||
else
|
||||
@@ -154,9 +161,9 @@ if [[ -f "$FILENAME" && "$FILENAME" == *.factory.bin ]]; then
|
||||
$ESPTOOL_CMD ${ESPTOOL_ERASE_FLASH}
|
||||
$ESPTOOL_CMD ${ESPTOOL_WRITE_FLASH} $FIRMWARE_OFFSET "${FILENAME}"
|
||||
echo "Trying to flash ${OTAFILE} at offset ${OTA_OFFSET}"
|
||||
$ESPTOOL_CMD ${ESPTOOL_WRITE_FLASH} $OTA_OFFSET "${OTAFILE}"
|
||||
echo "Trying to flash ${SPIFFSFILE}, at offset ${OFFSET}"
|
||||
$ESPTOOL_CMD ${ESPTOOL_WRITE_FLASH} $OFFSET "${SPIFFSFILE}"
|
||||
$ESPTOOL_CMD ${ESPTOOL_WRITE_FLASH} "$OTA_OFFSET" "${OTAFILE}"
|
||||
echo "Trying to flash ${SPIFFSFILE}, at offset ${SPIFFS_OFFSET}"
|
||||
$ESPTOOL_CMD ${ESPTOOL_WRITE_FLASH} "$SPIFFS_OFFSET" "${SPIFFSFILE}"
|
||||
|
||||
else
|
||||
show_help
|
||||
|
||||
@@ -340,6 +340,18 @@ for lb in env.GetLibBuilders():
|
||||
lb.env.Append(CPPDEFINES=[("APP_VERSION", verObj["long"])])
|
||||
break
|
||||
|
||||
# DEBUG_MUTE already compiles every LOG_* call out entirely; nanopb's own error-message
|
||||
# strings are a separate mechanism it doesn't touch, so mirror the same intent into nanopb.
|
||||
debug_mute_defined = any(
|
||||
d == "DEBUG_MUTE" or (isinstance(d, tuple) and d[0] == "DEBUG_MUTE") for d in env.get("CPPDEFINES", [])
|
||||
)
|
||||
if debug_mute_defined:
|
||||
projenv.Append(CPPDEFINES=[("PB_NO_ERRMSG", 1)])
|
||||
for lb in env.GetLibBuilders():
|
||||
if lb.name == "Nanopb":
|
||||
lb.env.Append(CPPDEFINES=[("PB_NO_ERRMSG", 1)])
|
||||
break
|
||||
|
||||
# Get the display resolution from macros
|
||||
def get_display_resolution(build_flags):
|
||||
# Check "DISPLAY_SIZE" to determine the screen resolution
|
||||
|
||||
@@ -69,7 +69,7 @@ message LoRaRegionPresetMap {
|
||||
### 2.3 Why grouped (and the size envelope clients should respect)
|
||||
|
||||
A `FromRadio` packet is capped at **512 bytes** (`MAX_TO_FROM_RADIO_SIZE`). Most regions
|
||||
share one identical preset list (the "standard" 9-preset list), so the map is delivered
|
||||
share one identical preset list (the "standard" 10-preset list), so the map is delivered
|
||||
**grouped**: `groups` holds each _distinct_ preset list once, and `region_groups` maps every
|
||||
known region to one of those groups by index. This keeps the encoded size additive
|
||||
(`groups` + `region_groups`) rather than multiplicative, well under the cap.
|
||||
@@ -153,7 +153,10 @@ These rules are what keep the UX correct across firmware versions. Implement all
|
||||
5. **EU region auto-swap caveat.** The firmware treats the EU sibling regions
|
||||
(`EU_868` / `EU_866` / `EU_N_868`) specially: if the user is in one of them and selects a
|
||||
preset that belongs to a sibling's list, the firmware **swaps the region** rather than
|
||||
rejecting the preset. Consequence for clients: **do not assume the region is immutable
|
||||
rejecting the preset. To make this visible in the picker, the firmware advertises the
|
||||
**same superset** (the union of the trio's presets) for all three sibling regions, so a
|
||||
client filtering per §6 will offer every EU 86x preset regardless of which sibling is
|
||||
currently selected. Consequence for clients: **do not assume the region is immutable
|
||||
across a preset change** - after an admin config write, re-read the resulting
|
||||
`LoRaConfig` and reflect the (possibly changed) region back into the UI.
|
||||
|
||||
@@ -246,29 +249,42 @@ For decoder unit tests. With the 2.8 region table, the firmware emits **6 groups
|
||||
indices are assigned in region-table order (first region to use a profile creates its group),
|
||||
so they are stable as listed here:
|
||||
|
||||
| group_index | default_preset | licensed_only | presets |
|
||||
| ----------------------- | -------------- | ------------- | -------------------------------------------------------------------------------------------------------------- |
|
||||
| 0 (standard) | `LONG_FAST` | false | LONG_FAST, LONG_SLOW, MEDIUM_SLOW, MEDIUM_FAST, SHORT_SLOW, SHORT_FAST, LONG_MODERATE, SHORT_TURBO, LONG_TURBO |
|
||||
| 1 (EU 868) | `LONG_FAST` | false | LONG_FAST, LONG_SLOW, MEDIUM_SLOW, MEDIUM_FAST, SHORT_SLOW, SHORT_FAST, LONG_MODERATE |
|
||||
| 2 (EU 866 SRD / "lite") | `LITE_FAST` | false | LITE_FAST, LITE_SLOW |
|
||||
| 3 (EU 868 narrow) | `NARROW_SLOW` | false | NARROW_FAST, NARROW_SLOW |
|
||||
| 4 (ham 20 kHz) | `TINY_FAST` | **true** | TINY_FAST, TINY_SLOW |
|
||||
| 5 (ham 100 kHz) | `NARROW_SLOW` | **true** | NARROW_FAST, NARROW_SLOW |
|
||||
| group_index | default_preset | licensed_only | presets |
|
||||
| ----------------------- | -------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 0 (standard) | `LONG_FAST` | false | LONG_FAST, LONG_SLOW, MEDIUM_SLOW, MEDIUM_FAST, SHORT_SLOW, SHORT_FAST, LONG_MODERATE, SHORT_TURBO, LONG_TURBO, MEDIUM_TURBO |
|
||||
| 1 (EU 868) | `LONG_FAST` | false | _EU 86x superset_ (see below) |
|
||||
| 2 (EU 866 SRD / "lite") | `LITE_FAST` | false | _EU 86x superset_ (see below) |
|
||||
| 3 (EU 868 narrow) | `NARROW_SLOW` | false | _EU 86x superset_ (see below) |
|
||||
| 4 (ham 20 kHz) | `TINY_FAST` | **true** | TINY_FAST, TINY_SLOW |
|
||||
| 5 (ham 100 kHz) | `NARROW_SLOW` | **true** | NARROW_FAST, NARROW_SLOW |
|
||||
|
||||
The **EU 86x superset** advertised by groups 1, 2 and 3 is the union of the trio's own
|
||||
band presets, because the firmware auto-swaps region within the trio on preset selection
|
||||
(§5), so any of these is a legal pick from any of the three regions:
|
||||
|
||||
```text
|
||||
LONG_FAST, LONG_SLOW, MEDIUM_SLOW, MEDIUM_FAST, SHORT_SLOW, SHORT_FAST, LONG_MODERATE, LITE_FAST, LITE_SLOW, NARROW_FAST, NARROW_SLOW
|
||||
```
|
||||
|
||||
The three groups still differ by `default_preset` (`LONG_FAST` / `LITE_FAST` / `NARROW_SLOW`),
|
||||
which is why they remain distinct groups despite sharing this preset list.
|
||||
|
||||
`region_groups` (region → group_index):
|
||||
|
||||
| group | regions |
|
||||
| ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 0 | US, EU_433, CN, JP, ANZ, ANZ_433, RU, KR, TW, IN, NZ_865, TH, UA_433, UA_868, MY_433, MY_919, SG_923, PH_433, PH_868, PH_915, KZ_433, KZ_863, NP_865, BR_902, LORA_24 |
|
||||
| 1 | EU_868 |
|
||||
| 2 | EU_866 |
|
||||
| 3 | EU_N_868 |
|
||||
| 4 | ITU1_2M, ITU2_2M, ITU3_2M |
|
||||
| 5 | ITU2_125CM |
|
||||
| group | regions |
|
||||
| ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 0 | US, EU_433, CN, JP, ANZ, ANZ_433, RU, KR, TW, IN, NZ_865, TH, UA_433, MY_433, MY_919, SG_923, PH_433, PH_868, PH_915, KZ_433, KZ_863, NP_865, BR_902, LORA_24 |
|
||||
| 1 | EU_868 |
|
||||
| 2 | EU_866 |
|
||||
| 3 | EU_N_868 |
|
||||
| 4 | ITU1_2M, ITU2_2M, ITU3_2M |
|
||||
| 5 | ITU2_125CM |
|
||||
|
||||
> Note groups **3** and **5** carry the same preset list (NARROW\_\*) but are distinct groups
|
||||
> because they differ in `licensed_only`. Decoders must key on the group, not on the preset
|
||||
> list, to preserve the licensing flag.
|
||||
> Note that several groups can carry overlapping preset lists but remain distinct: groups 1,
|
||||
> 2 and 3 share the EU 86x superset yet differ in `default_preset`, and group **5** (ham
|
||||
> 100 kHz) shares the `NARROW_*` presets with group 3 but differs in `licensed_only`.
|
||||
> Decoders must key on the group, not on the preset list, to preserve `default_preset` and
|
||||
> the licensing flag.
|
||||
>
|
||||
> Regions **absent** from the table (no constraint info; see §5.1): `EU_874`, `EU_917`,
|
||||
> `ITU1_70CM`, `ITU2_70CM`, `ITU3_70CM`.
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env python3
|
||||
# trunk-ignore-all(ruff/F821)
|
||||
# trunk-ignore-all(flake8/F821): For SConstruct imports
|
||||
#
|
||||
# PlatformIO routes build_flags to the compile step only, so the static link
|
||||
# flags are appended here, as wasm_link_flags.py does for [env:native-wasm].
|
||||
Import("env")
|
||||
|
||||
if env["PIOENV"].startswith("native-windows"):
|
||||
env.Append(
|
||||
LINKFLAGS=[
|
||||
"-static",
|
||||
"-static-libgcc",
|
||||
"-static-libstdc++",
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
# Layout for 4MB LILYGO T3-S3 boards (ESP32-S3FH4R2).
|
||||
# Reclaims 256KB from spiffs to give the app slot real headroom.
|
||||
# flashApp (ota_1) keeps its full 0xA0000 (655,360 B): the unified BLE OTA image
|
||||
# (meshtastic/esp32-unified-ota v1.0.1, mt-esp32s3-ota.bin) is 636,544 B, so this
|
||||
# slot cannot be shrunk to fund the app slot instead.
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x009000, 0x005000,
|
||||
otadata, data, ota, 0x00e000, 0x002000,
|
||||
app, app, ota_0, 0x010000, 0x290000,
|
||||
flashApp, app, ota_1, 0x2A0000, 0x0A0000,
|
||||
spiffs, data, spiffs, 0x340000, 0x0C0000,
|
||||
|
+5
-5
@@ -69,7 +69,7 @@ monitor_speed = 115200
|
||||
monitor_filters = direct
|
||||
lib_deps =
|
||||
# renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master
|
||||
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/2e26010040e028baee72e2093402fa7b3c59e430.zip
|
||||
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/9d9ba7e43ed1a5e1865fc9686513eab47fb079ff.zip
|
||||
# renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master
|
||||
https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip
|
||||
# renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master
|
||||
@@ -127,7 +127,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/effbb925a7cbd3ab794dfcc5fa16228217d18408.zip
|
||||
https://github.com/meshtastic/device-ui/archive/33917d583eb3f4d6f93a24122a88101221afcc2d.zip
|
||||
|
||||
; Common libs for environmental measurements in telemetry module
|
||||
[environmental_base]
|
||||
@@ -170,7 +170,7 @@ lib_deps =
|
||||
https://github.com/adafruit/Adafruit_TSL2591_Library/archive/refs/tags/1.4.5.zip
|
||||
# renovate: datasource=github-tags depName=EmotiBit MLX90632 packageName=emotibit/EmotiBit_MLX90632
|
||||
https://github.com/EmotiBit/EmotiBit_MLX90632/archive/refs/tags/v1.0.8.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit MLX90614 packageName=adafruit/Adafruit_MLX90614
|
||||
# renovate: datasource=github-tags depName=Adafruit MLX90614 packageName=adafruit/Adafruit-MLX90614-Library
|
||||
https://github.com/adafruit/Adafruit-MLX90614-Library/archive/refs/tags/2.1.6.zip
|
||||
# renovate: datasource=github-tags depName=INA3221_RT packageName=RobTillaart/INA3221_RT
|
||||
https://github.com/RobTillaart/INA3221_RT/archive/refs/tags/0.4.2.zip
|
||||
@@ -191,7 +191,7 @@ lib_deps =
|
||||
# renovate: datasource=github-tags depName=Adafruit LTR390 Library packageName=adafruit/Adafruit_LTR390
|
||||
https://github.com/adafruit/Adafruit_LTR390/archive/refs/tags/1.1.2.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit PCT2075 packageName=adafruit/Adafruit_PCT2075
|
||||
https://github.com/adafruit/Adafruit_PCT2075/archive/refs/tags/1.0.6.zip
|
||||
https://github.com/adafruit/Adafruit_PCT2075/archive/1.3.1.zip
|
||||
# renovate: datasource=github-tags depName=DFRobot_BMM150 packageName=dfrobot/DFRobot_BMM150
|
||||
https://github.com/DFRobot/DFRobot_BMM150/archive/refs/tags/V1.0.0.zip
|
||||
# renovate: datasource=github-tags depName=SparkFun MMC5983MA Magnetometer packageName=sparkfun/SparkFun_MMC5983MA_Magnetometer_Arduino_Library
|
||||
@@ -200,7 +200,7 @@ lib_deps =
|
||||
https://github.com/adafruit/Adafruit_TSL2561/archive/refs/tags/1.1.3.zip
|
||||
# renovate: datasource=github-tags depName=BH1750_WE packageName=wollewald/BH1750_WE
|
||||
https://github.com/wollewald/BH1750_WE/archive/refs/tags/1.1.10.zip
|
||||
# renovate: datasource=git-refs depName=Fusion packageName=https://github.com/meshtastic/Fusion gitBranch=master
|
||||
# renovate: datasource=git-refs depName=Fusion packageName=https://github.com/meshtastic/Fusion gitBranch=main
|
||||
https://github.com/meshtastic/Fusion/archive/936e1eb1e5ea19e8f4ed467526f91adeebb4f53c.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
|
||||
|
||||
+1
-1
Submodule protobufs updated: 1ae3be3d54...b6ad0e5f4e
@@ -27,6 +27,9 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC
|
||||
case PRESET(MEDIUM_FAST):
|
||||
return useShortName ? "MedF" : "MediumFast";
|
||||
break;
|
||||
case PRESET(MEDIUM_TURBO):
|
||||
return useShortName ? "MedT" : "MediumTurbo";
|
||||
break;
|
||||
case PRESET(LONG_SLOW):
|
||||
return useShortName ? "LongS" : "LongSlow";
|
||||
break;
|
||||
|
||||
+7
-2
@@ -57,11 +57,16 @@ using namespace Adafruit_LittleFS_Namespace;
|
||||
#endif
|
||||
|
||||
void fsInit();
|
||||
void fsListFiles();
|
||||
bool copyFile(const char *from, const char *to);
|
||||
bool renameFile(const char *pathFrom, const char *pathTo);
|
||||
bool fsFormat();
|
||||
std::vector<meshtastic_FileInfo> getFiles(const char *dirname, uint8_t levels, size_t maxCount = 64, bool *wasLimited = nullptr);
|
||||
void listDir(const char *dirname, uint8_t levels, bool del = false);
|
||||
void rmDir(const char *dirname);
|
||||
void setupSDCard();
|
||||
void setupSDCard();
|
||||
|
||||
#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) && defined(SDCARD_USE_SPI1)
|
||||
#include <SPI.h>
|
||||
// HSPI bus set up by setupSDCard(). Reuse this for other devices on the same bus.
|
||||
extern SPIClass SPI_HSPI;
|
||||
#endif
|
||||
+108
-21
@@ -7,6 +7,7 @@
|
||||
#include "SafeFile.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "memory/MemAudit.h"
|
||||
#include <cassert>
|
||||
#include <cstring> // memcpy
|
||||
|
||||
#ifndef MESSAGE_TEXT_POOL_SIZE
|
||||
@@ -65,6 +66,15 @@ static inline const char *getTextFromPool(uint16_t offset)
|
||||
return &g_messagePool[offset];
|
||||
}
|
||||
|
||||
static inline bool isIgnoredNodeNum(uint32_t nodeNum)
|
||||
{
|
||||
if (nodeNum == 0 || nodeNum == NODENUM_BROADCAST)
|
||||
return false;
|
||||
|
||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeNum);
|
||||
return nodeInfoLiteIsIgnored(node);
|
||||
}
|
||||
|
||||
// Helper: assign a timestamp (RTC if available, else boot-relative)
|
||||
static inline void assignTimestamp(StoredMessage &sm)
|
||||
{
|
||||
@@ -163,9 +173,44 @@ static inline void autosaveTick(MessageStore *store)
|
||||
}
|
||||
#endif
|
||||
|
||||
// Add from incoming/outgoing packet
|
||||
const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &packet)
|
||||
bool MessageStore::shouldStorePacket(const meshtastic_MeshPacket &packet) const
|
||||
{
|
||||
const uint32_t localNode = nodeDB->getNodeNum();
|
||||
const bool isDM = packet.to != 0 && packet.to != NODENUM_BROADCAST;
|
||||
if (isDM) {
|
||||
const bool outgoing = packet.from == 0 || packet.from == localNode;
|
||||
const uint32_t peer = outgoing ? packet.to : packet.from;
|
||||
return !isIgnoredNodeNum(peer);
|
||||
}
|
||||
|
||||
if (packet.from != 0 && packet.from != localNode)
|
||||
return !isIgnoredNodeNum(packet.from);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MessageStore::isMessageVisible(const StoredMessage &msg) const
|
||||
{
|
||||
const uint32_t localNode = nodeDB->getNodeNum();
|
||||
if (msg.type == MessageType::DM_TO_US) {
|
||||
const uint32_t peer = (msg.sender == localNode) ? msg.dest : msg.sender;
|
||||
return !isIgnoredNodeNum(peer);
|
||||
}
|
||||
|
||||
if (msg.sender != 0 && msg.sender != localNode)
|
||||
return !isIgnoredNodeNum(msg.sender);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Add from incoming/outgoing packet
|
||||
const StoredMessage *MessageStore::tryAddFromPacket(const meshtastic_MeshPacket &packet)
|
||||
{
|
||||
if (!shouldStorePacket(packet)) {
|
||||
LOG_DEBUG("Drop store 0x%08x", packet.from);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
StoredMessage sm;
|
||||
assignTimestamp(sm);
|
||||
sm.channelIndex = packet.channel;
|
||||
@@ -196,7 +241,14 @@ const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &pa
|
||||
markMessageStoreUnsaved();
|
||||
#endif
|
||||
|
||||
return liveMessages.back();
|
||||
return &liveMessages.back();
|
||||
}
|
||||
|
||||
const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &packet)
|
||||
{
|
||||
const StoredMessage *stored = tryAddFromPacket(packet);
|
||||
assert(stored);
|
||||
return *stored;
|
||||
}
|
||||
|
||||
// Outgoing/manual message
|
||||
@@ -323,28 +375,33 @@ void MessageStore::loadFromFlash()
|
||||
resetMessagePool(); // reset pool when loading
|
||||
|
||||
#ifdef FSCom
|
||||
concurrency::LockGuard guard(spiLock);
|
||||
{
|
||||
concurrency::LockGuard guard(spiLock);
|
||||
|
||||
if (!FSCom.exists(filename.c_str()))
|
||||
return;
|
||||
if (!FSCom.exists(filename.c_str()))
|
||||
return;
|
||||
|
||||
auto f = FSCom.open(filename.c_str(), FILE_O_READ);
|
||||
if (!f)
|
||||
return;
|
||||
auto f = FSCom.open(filename.c_str(), FILE_O_READ);
|
||||
if (!f)
|
||||
return;
|
||||
|
||||
uint8_t count = 0;
|
||||
f.readBytes(reinterpret_cast<char *>(&count), 1);
|
||||
if (count > MAX_MESSAGES_SAVED)
|
||||
count = MAX_MESSAGES_SAVED;
|
||||
uint8_t count = 0;
|
||||
f.readBytes(reinterpret_cast<char *>(&count), 1);
|
||||
if (count > MAX_MESSAGES_SAVED)
|
||||
count = MAX_MESSAGES_SAVED;
|
||||
|
||||
for (uint8_t i = 0; i < count; ++i) {
|
||||
StoredMessage m;
|
||||
if (!readMessageRecord(f, m))
|
||||
break;
|
||||
liveMessages.push_back(m);
|
||||
for (uint8_t i = 0; i < count; ++i) {
|
||||
StoredMessage m;
|
||||
if (!readMessageRecord(f, m))
|
||||
break;
|
||||
liveMessages.push_back(m);
|
||||
}
|
||||
|
||||
f.close();
|
||||
}
|
||||
|
||||
f.close();
|
||||
if (pruneHiddenMessages())
|
||||
saveToFlash();
|
||||
#endif
|
||||
// Loading messages does not trigger an autosave
|
||||
g_messageStoreHasUnsavedChanges = false;
|
||||
@@ -406,6 +463,13 @@ template <typename Predicate> static void eraseAllMatches(std::deque<StoredMessa
|
||||
}
|
||||
}
|
||||
|
||||
bool MessageStore::pruneHiddenMessages()
|
||||
{
|
||||
const size_t before = liveMessages.size();
|
||||
eraseAllMatches(liveMessages, [&](const StoredMessage &m) { return !isMessageVisible(m); });
|
||||
return liveMessages.size() != before;
|
||||
}
|
||||
|
||||
// Delete oldest message (RAM + persisted queue)
|
||||
void MessageStore::deleteOldestMessage()
|
||||
{
|
||||
@@ -443,6 +507,20 @@ void MessageStore::deleteAllMessagesWithPeer(uint32_t peer)
|
||||
saveToFlash();
|
||||
}
|
||||
|
||||
void MessageStore::deleteAllMessagesFromNode(uint32_t nodeNum)
|
||||
{
|
||||
const uint32_t local = nodeDB->getNodeNum();
|
||||
auto pred = [&](const StoredMessage &m) {
|
||||
if (m.sender == nodeNum)
|
||||
return true;
|
||||
if (m.type != MessageType::DM_TO_US)
|
||||
return false;
|
||||
return m.sender == local ? m.dest == nodeNum : m.sender == nodeNum;
|
||||
};
|
||||
eraseAllMatches(liveMessages, pred);
|
||||
saveToFlash();
|
||||
}
|
||||
|
||||
// Delete oldest message in a direct chat with a node
|
||||
void MessageStore::deleteOldestMessageWithPeer(uint32_t peer)
|
||||
{
|
||||
@@ -460,7 +538,7 @@ std::deque<StoredMessage> MessageStore::getChannelMessages(uint8_t channel) cons
|
||||
{
|
||||
std::deque<StoredMessage> result;
|
||||
for (const auto &m : liveMessages) {
|
||||
if (m.type == MessageType::BROADCAST && m.channelIndex == channel) {
|
||||
if (isMessageVisible(m) && m.type == MessageType::BROADCAST && m.channelIndex == channel) {
|
||||
result.push_back(m);
|
||||
}
|
||||
}
|
||||
@@ -471,13 +549,22 @@ std::deque<StoredMessage> MessageStore::getDirectMessages() const
|
||||
{
|
||||
std::deque<StoredMessage> result;
|
||||
for (const auto &m : liveMessages) {
|
||||
if (m.type == MessageType::DM_TO_US) {
|
||||
if (isMessageVisible(m) && m.type == MessageType::DM_TO_US) {
|
||||
result.push_back(m);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool MessageStore::hasVisibleMessages() const
|
||||
{
|
||||
for (const auto &m : liveMessages) {
|
||||
if (isMessageVisible(m))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Upgrade boot-relative timestamps once RTC is valid
|
||||
// Only same-boot boot-relative messages are healed.
|
||||
// Persisted boot-relative messages from old boots stay ??? forever.
|
||||
|
||||
+6
-2
@@ -93,7 +93,7 @@ class MessageStore
|
||||
void addLiveMessage(StoredMessage &&msg);
|
||||
void addLiveMessage(const StoredMessage &msg); // convenience overload
|
||||
const std::deque<StoredMessage> &getLiveMessages() const { return liveMessages; }
|
||||
|
||||
const StoredMessage *tryAddFromPacket(const meshtastic_MeshPacket &mp); // Incoming/outgoing -> RAM only
|
||||
// Add new messages from packets or manual input
|
||||
const StoredMessage &addFromPacket(const meshtastic_MeshPacket &mp); // Incoming/outgoing → RAM only
|
||||
void addFromString(uint32_t sender, uint8_t channelIndex, const std::string &text); // Manual add
|
||||
@@ -111,13 +111,16 @@ class MessageStore
|
||||
void deleteOldestMessageWithPeer(uint32_t peer);
|
||||
void deleteAllMessagesInChannel(uint8_t channel);
|
||||
void deleteAllMessagesWithPeer(uint32_t peer);
|
||||
|
||||
void deleteAllMessagesFromNode(uint32_t nodeNum);
|
||||
// Unified accessor (for UI code, defaults to RAM buffer)
|
||||
const std::deque<StoredMessage> &getMessages() const { return liveMessages; }
|
||||
bool hasVisibleMessages() const;
|
||||
|
||||
// Helper filters for future use
|
||||
std::deque<StoredMessage> getChannelMessages(uint8_t channel) const; // Only broadcast messages on a channel
|
||||
std::deque<StoredMessage> getDirectMessages() const; // Only direct messages
|
||||
bool shouldStorePacket(const meshtastic_MeshPacket &mp) const;
|
||||
bool isMessageVisible(const StoredMessage &msg) const;
|
||||
|
||||
// Upgrade boot-relative timestamps once RTC is valid
|
||||
void upgradeBootRelativeTimestamps();
|
||||
@@ -129,6 +132,7 @@ class MessageStore
|
||||
static uint16_t storeText(const char *src, size_t len);
|
||||
|
||||
private:
|
||||
bool pruneHiddenMessages();
|
||||
std::deque<StoredMessage> liveMessages; // Single in-RAM message buffer (also used for persistence)
|
||||
std::string filename; // Flash filename for persistence
|
||||
};
|
||||
|
||||
+4
-1
@@ -37,6 +37,7 @@
|
||||
#if defined(ARCH_PORTDUINO)
|
||||
#include "api/WiFiServerAPI.h"
|
||||
#include "input/LinuxInputImpl.h"
|
||||
#include "input/LinuxJoystick.h"
|
||||
#endif
|
||||
|
||||
// Working USB detection for powered/charging states on the RAK platform
|
||||
@@ -860,6 +861,8 @@ void Power::reboot()
|
||||
#ifdef __linux__
|
||||
if (aLinuxInputImpl)
|
||||
aLinuxInputImpl->deInit();
|
||||
if (aLinuxJoystick)
|
||||
aLinuxJoystick->deInit();
|
||||
#endif
|
||||
SPI.end();
|
||||
Wire.end();
|
||||
@@ -903,7 +906,7 @@ void Power::shutdown()
|
||||
#if HAS_SCREEN
|
||||
messageStore.saveToFlash();
|
||||
#endif
|
||||
#if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040)
|
||||
#if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)
|
||||
#ifdef PIN_LED1
|
||||
ledOff(PIN_LED1);
|
||||
#endif
|
||||
|
||||
@@ -51,9 +51,6 @@ class PowerStatus : public Status
|
||||
|
||||
bool getHasUSB() const { return hasUSB == OptTrue; }
|
||||
|
||||
/// Can we even know if this board has USB power or not
|
||||
bool knowsUSB() const { return hasUSB != OptUnknown; }
|
||||
|
||||
bool getIsCharging() const { return isCharging == OptTrue; }
|
||||
|
||||
int getBatteryVoltageMv() const { return batteryVoltageMv; }
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#include "RedirectablePrint.h"
|
||||
#include "NodeDB.h"
|
||||
#include "RTC.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "main.h"
|
||||
#include "memGet.h"
|
||||
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||
|
||||
+116
-10
@@ -3,7 +3,9 @@
|
||||
#include "NodeDB.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "Throttle.h"
|
||||
#include "concurrency/LockGuard.h"
|
||||
#include "configuration.h"
|
||||
#include "main.h"
|
||||
#include "time.h"
|
||||
|
||||
#if defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT
|
||||
@@ -28,6 +30,17 @@
|
||||
|
||||
SerialConsole *console;
|
||||
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
// Last-seen USB-CDC host link (DTR/mount) state, sampled each runOnce() so a
|
||||
// physical unplug/replug re-locks the per-connection admin auth (see runOnce()).
|
||||
// Kept at file scope rather than as a member both because there is exactly one
|
||||
// console singleton and because adding per-instance members to the PhoneAPI
|
||||
// hierarchy has historically perturbed nRF52 USB-CDC enumeration (see PhoneAPI.h).
|
||||
// Only compiled on lockdown (nRF52) builds.
|
||||
static bool s_serialLinkUp = false;
|
||||
#endif
|
||||
|
||||
/// Create the shared serial console once and register receive wakeups.
|
||||
void consoleInit()
|
||||
{
|
||||
if (console) {
|
||||
@@ -44,6 +57,7 @@ void consoleInit()
|
||||
DEBUG_PORT.rpInit(); // Simply sets up semaphore
|
||||
}
|
||||
|
||||
/// Print and flush an unclassified formatted console message.
|
||||
void consolePrintf(const char *format, ...)
|
||||
{
|
||||
va_list arg;
|
||||
@@ -53,6 +67,7 @@ void consolePrintf(const char *format, ...)
|
||||
console->flush();
|
||||
}
|
||||
|
||||
/// Initialize console, protobuf transport, serial port, and worker thread state.
|
||||
SerialConsole::SerialConsole() : StreamAPI(&Port), RedirectablePrint(&Port), concurrency::OSThread("SerialConsole")
|
||||
{
|
||||
api_type = TYPE_SERIAL;
|
||||
@@ -80,8 +95,33 @@ SerialConsole::SerialConsole() : StreamAPI(&Port), RedirectablePrint(&Port), con
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Service one serial API iteration and select the next polling interval.
|
||||
int32_t SerialConsole::runOnce()
|
||||
{
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
// Lockdown (nRF52) builds only. The SerialConsole is a process-lifetime
|
||||
// singleton, so its inherited PhoneAPI object - and therefore its entry in
|
||||
// the per-connection admin-auth slot table (keyed by PhoneAPI*) - is reused
|
||||
// for every USB/serial client for the whole boot. Nothing re-locks that slot
|
||||
// when the operator unplugs and a different client plugs in before the
|
||||
// 15-minute inactivity timeout fires, so a fresh client would inherit the
|
||||
// prior operator's admin authorization. Re-lock when the physical USB-CDC link
|
||||
// drops - the serial analog of the BLE onDisconnect() -> close() session reset.
|
||||
//
|
||||
// On the nRF52 TinyUSB (Adafruit) core, (bool)Port == tud_cdc_n_connected():
|
||||
// it goes false on cable unplug or host port-close (DTR de-assert). close()
|
||||
// frees the auth slot and resets PhoneAPI state, so whoever connects next
|
||||
// re-locks via handleStartConfig()'s !isConnected() branch on their first
|
||||
// want_config - the same physical-link boundary BLE enforces in onConnect().
|
||||
// Console transports without a real DTR line (e.g. a UART USER_DEBUG_PORT) hold
|
||||
// this constant, so no edge fires and we fall back to the existing inactivity
|
||||
// timeout - no worse than the pre-fix behavior.
|
||||
const bool linkUp = static_cast<bool>(Port);
|
||||
if (s_serialLinkUp && !linkUp)
|
||||
close();
|
||||
s_serialLinkUp = linkUp;
|
||||
#endif
|
||||
|
||||
#ifdef HELTEC_MESH_SOLAR
|
||||
// After enabling the mesh solar serial port module configuration, command processing is handled by the serial port module.
|
||||
if (moduleConfig.serial.enabled && moduleConfig.serial.override_console_serial_port &&
|
||||
@@ -100,29 +140,48 @@ int32_t SerialConsole::runOnce()
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Flush raw output while preserving queued protobuf frames.
|
||||
void SerialConsole::flush()
|
||||
{
|
||||
// HWCDC::flush()'s no-progress path discards queued TX bytes, which would tear a
|
||||
// framed protobuf stream; framed output is drained by the TX interrupt instead.
|
||||
if (usingProtobufs)
|
||||
return;
|
||||
|
||||
Port.flush();
|
||||
}
|
||||
|
||||
// trigger tx of serial data
|
||||
/// Write raw console data only before protobuf framing becomes active.
|
||||
size_t SerialConsole::write(uint8_t c)
|
||||
{
|
||||
// Once a protobuf client is active, unframed bytes would corrupt its stream.
|
||||
if (usingProtobufs)
|
||||
return 1;
|
||||
|
||||
if (c == '\n')
|
||||
RedirectablePrint::write('\r');
|
||||
return RedirectablePrint::write(c);
|
||||
}
|
||||
|
||||
/// Wake the serial worker when PhoneAPI queues output.
|
||||
void SerialConsole::onNowHasData(uint32_t fromRadioNum)
|
||||
{
|
||||
setIntervalFromNow(0);
|
||||
}
|
||||
|
||||
// trigger rx of serial data
|
||||
/// Wake the serial worker when receive activity is signaled.
|
||||
void SerialConsole::rxInt()
|
||||
{
|
||||
setIntervalFromNow(0);
|
||||
}
|
||||
|
||||
// For the serial port we can't really detect if any client is on the other side, so instead just look for recent messages
|
||||
/// Infer serial client connectivity from recent API contact.
|
||||
bool SerialConsole::checkIsConnected()
|
||||
{
|
||||
return Throttle::isWithinTimespanMs(lastContactMsec, SERIAL_CONNECTION_TIMEOUT);
|
||||
}
|
||||
|
||||
/// Select bounded or non-blocking HWCDC writes based on host liveness.
|
||||
void SerialConsole::setHostDraining(bool draining)
|
||||
{
|
||||
#ifdef IS_USB_SERIAL
|
||||
@@ -134,17 +193,59 @@ void SerialConsole::setHostDraining(bool draining)
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Update HWCDC timeout mode around generic connection handling.
|
||||
void SerialConsole::onConnectionChanged(bool connected)
|
||||
{
|
||||
// Order matters on disconnect: make console TX non-blocking *before* the
|
||||
// PowerFSM/close handling below emits more log lines to a dead port.
|
||||
if (!connected)
|
||||
if (!connected) {
|
||||
setHostDraining(false);
|
||||
// Keep any retained tail: HWCDC may still hold its prefix, and dropping metadata
|
||||
// would let the next frame header land inside that frame's declared payload.
|
||||
}
|
||||
StreamAPI::onConnectionChanged(connected);
|
||||
if (connected)
|
||||
setHostDraining(true);
|
||||
}
|
||||
|
||||
/// Continue retained USB CDC output under the shared stream lock.
|
||||
bool SerialConsole::finishPendingFrame()
|
||||
{
|
||||
#ifdef IS_USB_SERIAL
|
||||
concurrency::LockGuard guard(&streamLock);
|
||||
return frameWriter.finishPendingFrame(Port);
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Protect the retained log buffer from being overwritten.
|
||||
bool SerialConsole::canEncodeLogRecord()
|
||||
{
|
||||
#ifdef IS_USB_SERIAL
|
||||
concurrency::LockGuard guard(&streamLock);
|
||||
return frameWriter.isIdle();
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Frame USB CDC output and retain any unwritten tail.
|
||||
bool SerialConsole::writeFrame(uint8_t *buf, size_t len, bool bestEffort)
|
||||
{
|
||||
#ifdef IS_USB_SERIAL
|
||||
if (len == 0 || !canWrite)
|
||||
return false;
|
||||
|
||||
const size_t totalLen = buildFrameHeader(buf, len);
|
||||
|
||||
concurrency::LockGuard guard(&streamLock);
|
||||
return frameWriter.writeFrame(Port, buf, totalLen, bestEffort);
|
||||
#else
|
||||
return StreamAPI::writeFrame(buf, len, bestEffort);
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* we override this to notice when we've received a protobuf over the serial
|
||||
* stream. Then we shut off debug serial output.
|
||||
@@ -167,12 +268,17 @@ bool SerialConsole::handleToRadio(const uint8_t *buf, size_t len)
|
||||
}
|
||||
}
|
||||
|
||||
/// Route logs without allowing raw bytes into an active protobuf stream.
|
||||
void SerialConsole::log_to_serial(const char *logLevel, const char *format, va_list arg)
|
||||
{
|
||||
if (usingProtobufs && config.security.debug_log_api_enabled) {
|
||||
meshtastic_LogRecord_Level ll = RedirectablePrint::getLogLevel(logLevel);
|
||||
auto thread = concurrency::OSThread::currentThread;
|
||||
emitLogRecord(ll, thread ? thread->ThreadName.c_str() : "", format, arg);
|
||||
} else
|
||||
RedirectablePrint::log_to_serial(logLevel, format, arg);
|
||||
if (usingProtobufs) {
|
||||
if (config.security.debug_log_api_enabled && !pauseBluetoothLogging) {
|
||||
meshtastic_LogRecord_Level ll = RedirectablePrint::getLogLevel(logLevel);
|
||||
auto thread = concurrency::OSThread::currentThread;
|
||||
emitLogRecord(ll, thread ? thread->ThreadName.c_str() : "", format, arg);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
RedirectablePrint::log_to_serial(logLevel, format, arg);
|
||||
}
|
||||
|
||||
+20
-7
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "RedirectablePrint.h"
|
||||
#include "StreamAPI.h"
|
||||
#include "mesh/StreamFrameWriter.h"
|
||||
/**
|
||||
* Provides both debug printing and, if the client starts sending protobufs to us, switches to send/receive protobufs
|
||||
* (and starts dropping debug printing - FIXME, eventually those prints should be encapsulated in protobufs).
|
||||
@@ -14,6 +15,7 @@ class SerialConsole : public StreamAPI, public RedirectablePrint, private concur
|
||||
bool usingProtobufs = false;
|
||||
|
||||
public:
|
||||
/// Initialize the shared serial stream for console and protobuf traffic.
|
||||
SerialConsole();
|
||||
|
||||
/**
|
||||
@@ -22,35 +24,46 @@ class SerialConsole : public StreamAPI, public RedirectablePrint, private concur
|
||||
*/
|
||||
virtual bool handleToRadio(const uint8_t *buf, size_t len) override;
|
||||
|
||||
virtual size_t write(uint8_t c) override
|
||||
{
|
||||
if (c == '\n') // prefix any newlines with carriage return
|
||||
RedirectablePrint::write('\r');
|
||||
return RedirectablePrint::write(c);
|
||||
}
|
||||
/// Write a raw console byte unless the stream is in protobuf mode.
|
||||
virtual size_t write(uint8_t c) override;
|
||||
|
||||
/// Service serial input, pending output, and connection state.
|
||||
virtual int32_t runOnce() override;
|
||||
|
||||
/// Flush raw console output when explicitly requested.
|
||||
void flush();
|
||||
/// Wake the serial thread after receive activity.
|
||||
void rxInt();
|
||||
|
||||
protected:
|
||||
/// Check the current underlying physical link to see if the client is currently connected
|
||||
virtual bool checkIsConnected() override;
|
||||
|
||||
/// Wake the serial thread when PhoneAPI queues output.
|
||||
virtual void onNowHasData(uint32_t fromRadioNum) override;
|
||||
|
||||
/// Track serial API connect/disconnect so we can make console writes
|
||||
/// non-blocking while no host is listening (see setHostDraining()).
|
||||
virtual void onConnectionChanged(bool connected) override;
|
||||
|
||||
/// Possibly switch to protobufs if we see a valid protobuf message
|
||||
/// Emit a framed API log when enabled, or raw output before protobuf mode.
|
||||
virtual void log_to_serial(const char *logLevel, const char *format, va_list arg);
|
||||
|
||||
/// Continue retained USB CDC output before PhoneAPI advances.
|
||||
virtual bool finishPendingFrame() override;
|
||||
/// Return whether the dedicated log buffer can be safely overwritten.
|
||||
virtual bool canEncodeLogRecord() override;
|
||||
/// Write or retain one framed USB CDC message.
|
||||
virtual bool writeFrame(uint8_t *buf, size_t len, bool bestEffort) override;
|
||||
|
||||
private:
|
||||
/// On USB CDC targets, keep console TX non-blocking unless a host is draining the
|
||||
/// port, so a dead host can't stall the main loop and trip the task watchdog.
|
||||
void setHostDraining(bool draining);
|
||||
|
||||
#if defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT
|
||||
StreamFrameWriter frameWriter;
|
||||
#endif
|
||||
};
|
||||
|
||||
// A simple wrapper to allow non class aware code write to the console
|
||||
|
||||
@@ -88,6 +88,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define MESHTASTIC_PREHOP_DROP 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
|
||||
// any shipping build; enable per-build with -D MESHTASTIC_ENABLE_FRAME_INJECTION=1.
|
||||
#ifndef MESHTASTIC_ENABLE_FRAME_INJECTION
|
||||
#define MESHTASTIC_ENABLE_FRAME_INJECTION 0
|
||||
#endif
|
||||
|
||||
/// Convert a preprocessor name into a quoted string
|
||||
#define xstr(s) ystr(s)
|
||||
#define ystr(s) #s
|
||||
@@ -416,6 +424,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#ifndef HAS_TFT
|
||||
#define HAS_TFT 0
|
||||
#endif
|
||||
// Opt-in: build the BaseUI games frame (Snake). Off by default; enable per build/variant with
|
||||
// -DBASEUI_HAS_GAMES=1 (requires HAS_SCREEN and a non-color BaseUI display).
|
||||
#ifndef BASEUI_HAS_GAMES
|
||||
#define BASEUI_HAS_GAMES 0
|
||||
#endif
|
||||
#ifndef HAS_WIRE
|
||||
#define HAS_WIRE 0
|
||||
#endif
|
||||
|
||||
+1
-1
@@ -8,10 +8,10 @@
|
||||
#include "GpioLogic.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerMon.h"
|
||||
#include "RTC.h"
|
||||
#include "Throttle.h"
|
||||
#include "buzz.h"
|
||||
#include "concurrency/Periodic.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "meshUtils.h"
|
||||
|
||||
#include "main.h" // pmu_found
|
||||
|
||||
+38
-5
@@ -36,19 +36,52 @@ GeoCoord::GeoCoord(double lat, double lon, int32_t alt) : _altitude(alt)
|
||||
GeoCoord::setCoords();
|
||||
}
|
||||
|
||||
// Initialize all the coordinate systems
|
||||
// Initialize DMS eagerly (cheap, most commonly used); UTM/MGRS/OSGR/OLC are computed lazily on
|
||||
// first access via their getters (see ensure*() below), since most callers never touch them.
|
||||
void GeoCoord::setCoords()
|
||||
{
|
||||
double lat = _latitude * 1e-7;
|
||||
double lon = _longitude * 1e-7;
|
||||
GeoCoord::latLongToDMS(lat, lon, _dms);
|
||||
GeoCoord::latLongToUTM(lat, lon, _utm);
|
||||
GeoCoord::latLongToMGRS(lat, lon, _mgrs);
|
||||
GeoCoord::latLongToOSGR(lat, lon, _osgr);
|
||||
GeoCoord::latLongToOLC(lat, lon, _olc);
|
||||
_utmValid = false;
|
||||
_mgrsValid = false;
|
||||
_osgrValid = false;
|
||||
_olcValid = false;
|
||||
_dirty = false;
|
||||
}
|
||||
|
||||
void GeoCoord::ensureUTM() const
|
||||
{
|
||||
if (!_utmValid) {
|
||||
GeoCoord::latLongToUTM(_latitude * 1e-7, _longitude * 1e-7, _utm);
|
||||
_utmValid = true;
|
||||
}
|
||||
}
|
||||
|
||||
void GeoCoord::ensureMGRS() const
|
||||
{
|
||||
if (!_mgrsValid) {
|
||||
GeoCoord::latLongToMGRS(_latitude * 1e-7, _longitude * 1e-7, _mgrs);
|
||||
_mgrsValid = true;
|
||||
}
|
||||
}
|
||||
|
||||
void GeoCoord::ensureOSGR() const
|
||||
{
|
||||
if (!_osgrValid) {
|
||||
GeoCoord::latLongToOSGR(_latitude * 1e-7, _longitude * 1e-7, _osgr);
|
||||
_osgrValid = true;
|
||||
}
|
||||
}
|
||||
|
||||
void GeoCoord::ensureOLC() const
|
||||
{
|
||||
if (!_olcValid) {
|
||||
GeoCoord::latLongToOLC(_latitude * 1e-7, _longitude * 1e-7, _olc);
|
||||
_olcValid = true;
|
||||
}
|
||||
}
|
||||
|
||||
void GeoCoord::updateCoords(int32_t lat, int32_t lon, int32_t alt)
|
||||
{
|
||||
// If marked dirty or new coordinates
|
||||
|
||||
+93
-23
@@ -65,14 +65,24 @@ class GeoCoord
|
||||
int32_t _altitude = 0;
|
||||
|
||||
DMS _dms = {};
|
||||
UTM _utm = {};
|
||||
MGRS _mgrs = {};
|
||||
OSGR _osgr = {};
|
||||
OLC _olc = {};
|
||||
// Computed lazily on first access via ensure*() below; mutable so const getters can populate
|
||||
// them on demand.
|
||||
mutable UTM _utm = {};
|
||||
mutable MGRS _mgrs = {};
|
||||
mutable OSGR _osgr = {};
|
||||
mutable OLC _olc = {};
|
||||
mutable bool _utmValid = false;
|
||||
mutable bool _mgrsValid = false;
|
||||
mutable bool _osgrValid = false;
|
||||
mutable bool _olcValid = false;
|
||||
|
||||
bool _dirty = true;
|
||||
|
||||
void setCoords();
|
||||
void ensureUTM() const;
|
||||
void ensureMGRS() const;
|
||||
void ensureOSGR() const;
|
||||
void ensureOLC() const;
|
||||
|
||||
public:
|
||||
GeoCoord();
|
||||
@@ -123,26 +133,86 @@ class GeoCoord
|
||||
uint32_t getDMSLonSec() const { return _dms.lonSec; }
|
||||
char getDMSLonCP() const { return _dms.lonCP; }
|
||||
|
||||
// UTM getters
|
||||
uint8_t getUTMZone() const { return _utm.zone; }
|
||||
char getUTMBand() const { return _utm.band; }
|
||||
uint32_t getUTMEasting() const { return _utm.easting; }
|
||||
uint32_t getUTMNorthing() const { return _utm.northing; }
|
||||
// UTM getters (computed on first access - see ensureUTM())
|
||||
uint8_t getUTMZone() const
|
||||
{
|
||||
ensureUTM();
|
||||
return _utm.zone;
|
||||
}
|
||||
char getUTMBand() const
|
||||
{
|
||||
ensureUTM();
|
||||
return _utm.band;
|
||||
}
|
||||
uint32_t getUTMEasting() const
|
||||
{
|
||||
ensureUTM();
|
||||
return _utm.easting;
|
||||
}
|
||||
uint32_t getUTMNorthing() const
|
||||
{
|
||||
ensureUTM();
|
||||
return _utm.northing;
|
||||
}
|
||||
|
||||
// MGRS getters
|
||||
uint8_t getMGRSZone() const { return _mgrs.zone; }
|
||||
char getMGRSBand() const { return _mgrs.band; }
|
||||
char getMGRSEast100k() const { return _mgrs.east100k; }
|
||||
char getMGRSNorth100k() const { return _mgrs.north100k; }
|
||||
uint32_t getMGRSEasting() const { return _mgrs.easting; }
|
||||
uint32_t getMGRSNorthing() const { return _mgrs.northing; }
|
||||
// MGRS getters (computed on first access - see ensureMGRS())
|
||||
uint8_t getMGRSZone() const
|
||||
{
|
||||
ensureMGRS();
|
||||
return _mgrs.zone;
|
||||
}
|
||||
char getMGRSBand() const
|
||||
{
|
||||
ensureMGRS();
|
||||
return _mgrs.band;
|
||||
}
|
||||
char getMGRSEast100k() const
|
||||
{
|
||||
ensureMGRS();
|
||||
return _mgrs.east100k;
|
||||
}
|
||||
char getMGRSNorth100k() const
|
||||
{
|
||||
ensureMGRS();
|
||||
return _mgrs.north100k;
|
||||
}
|
||||
uint32_t getMGRSEasting() const
|
||||
{
|
||||
ensureMGRS();
|
||||
return _mgrs.easting;
|
||||
}
|
||||
uint32_t getMGRSNorthing() const
|
||||
{
|
||||
ensureMGRS();
|
||||
return _mgrs.northing;
|
||||
}
|
||||
|
||||
// OSGR getters
|
||||
char getOSGRE100k() const { return _osgr.e100k; }
|
||||
char getOSGRN100k() const { return _osgr.n100k; }
|
||||
uint32_t getOSGREasting() const { return _osgr.easting; }
|
||||
uint32_t getOSGRNorthing() const { return _osgr.northing; }
|
||||
// OSGR getters (computed on first access - see ensureOSGR())
|
||||
char getOSGRE100k() const
|
||||
{
|
||||
ensureOSGR();
|
||||
return _osgr.e100k;
|
||||
}
|
||||
char getOSGRN100k() const
|
||||
{
|
||||
ensureOSGR();
|
||||
return _osgr.n100k;
|
||||
}
|
||||
uint32_t getOSGREasting() const
|
||||
{
|
||||
ensureOSGR();
|
||||
return _osgr.easting;
|
||||
}
|
||||
uint32_t getOSGRNorthing() const
|
||||
{
|
||||
ensureOSGR();
|
||||
return _osgr.northing;
|
||||
}
|
||||
|
||||
// OLC getter
|
||||
void getOLCCode(char *code) { strncpy(code, _olc.code, OLC_CODE_LEN + 1); } // +1 for null termination
|
||||
// OLC getter (computed on first access - see ensureOLC())
|
||||
void getOLCCode(char *code)
|
||||
{
|
||||
ensureOLC();
|
||||
strncpy(code, _olc.code, OLC_CODE_LEN + 1); // +1 for null termination
|
||||
}
|
||||
};
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
#if !MESHTASTIC_EXCLUDE_GPS
|
||||
#include "NMEAWPL.h"
|
||||
#include "GeoCoord.h"
|
||||
#include "RTC.h"
|
||||
#include "gps/RTC.h"
|
||||
#include <time.h>
|
||||
|
||||
/* -------------------------------------------
|
||||
|
||||
+45
-4
@@ -1,4 +1,4 @@
|
||||
#include "RTC.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "configuration.h"
|
||||
#include "detect/ScanI2C.h"
|
||||
#include "main.h"
|
||||
@@ -7,6 +7,10 @@
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
|
||||
#if HAS_LSE
|
||||
#include <STM32RTC.h>
|
||||
#endif
|
||||
|
||||
static RTCQuality currentQuality = RTCQualityNone;
|
||||
uint32_t lastSetFromPhoneNtpOrGps = 0;
|
||||
|
||||
@@ -211,6 +215,30 @@ RTCSetResult readFromRTC()
|
||||
return RTCSetResultSuccess;
|
||||
}
|
||||
}
|
||||
#elif HAS_LSE
|
||||
if (stm32wlRtcAvailable()) {
|
||||
uint32_t now = millis();
|
||||
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);
|
||||
lastTimeValidationWarning = millis();
|
||||
}
|
||||
return RTCSetResultInvalidTime;
|
||||
}
|
||||
#endif
|
||||
if (currentQuality == RTCQualityNone) {
|
||||
RTCQuality oldQuality = currentQuality;
|
||||
timeStartMsec = now;
|
||||
zeroOffsetSecs = tv.tv_sec;
|
||||
currentQuality = RTCQualityDevice;
|
||||
triggerNodeInfoCheckOnTimeSource(oldQuality, currentQuality);
|
||||
}
|
||||
return RTCSetResultSuccess;
|
||||
}
|
||||
#else
|
||||
return readFromSystemTimeFallback();
|
||||
#endif
|
||||
@@ -291,7 +319,10 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
|
||||
#else
|
||||
rtc.initI2C();
|
||||
#endif
|
||||
tm *t = gmtime(&tv->tv_sec);
|
||||
// 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);
|
||||
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);
|
||||
@@ -313,7 +344,10 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
|
||||
#else
|
||||
rtc.begin(Wire);
|
||||
#endif
|
||||
tm *t = gmtime(&tv->tv_sec);
|
||||
// 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);
|
||||
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);
|
||||
@@ -327,7 +361,10 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
|
||||
#else
|
||||
ArtronShop_RX8130CE rtc(&Wire);
|
||||
#endif
|
||||
tm *t = gmtime(&tv->tv_sec);
|
||||
// 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);
|
||||
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);
|
||||
@@ -335,6 +372,10 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
|
||||
LOG_WARN("Failed to set time for RX8130CE");
|
||||
}
|
||||
}
|
||||
#elif HAS_LSE
|
||||
if (stm32wlRtcAvailable()) {
|
||||
STM32RTC::getInstance().setEpoch(tv->tv_sec);
|
||||
}
|
||||
#elif defined(ARCH_ESP32) || defined(ARCH_RP2040)
|
||||
settimeofday(tv, NULL);
|
||||
#endif
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
#include <ArtronShop_RX8130CE.h>
|
||||
#endif
|
||||
|
||||
#if HAS_LSE
|
||||
// True once the STM32WL LSE crystal has locked and the hardware RTC is running (see stm32wlSetup()).
|
||||
bool stm32wlRtcAvailable();
|
||||
#endif
|
||||
|
||||
enum RTCQuality {
|
||||
|
||||
/// We haven't had our RTC set yet
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#if defined(USE_EINK) && !defined(USE_EINK_PARALLELDISPLAY)
|
||||
#include "EInkDisplay2.h"
|
||||
#include "FSCommon.h"
|
||||
#include "SPILock.h"
|
||||
#include "main.h"
|
||||
#include <SPI.h>
|
||||
@@ -214,9 +215,23 @@ bool EInkDisplay::connect()
|
||||
defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER) || \
|
||||
defined(MINI_EPAPER_S3)
|
||||
{
|
||||
#if defined(TLORA_T3S3_EPAPER)
|
||||
// T3-S3 shares HSPI with the SD card; preconfigure the panel control pins.
|
||||
hspi = &SPI_HSPI;
|
||||
pinMode(PIN_EINK_CS, OUTPUT);
|
||||
pinMode(PIN_EINK_DC, OUTPUT);
|
||||
pinMode(PIN_EINK_BUSY, INPUT);
|
||||
if (PIN_EINK_RES >= 0) {
|
||||
pinMode(PIN_EINK_RES, OUTPUT);
|
||||
digitalWrite(PIN_EINK_RES, HIGH);
|
||||
}
|
||||
digitalWrite(PIN_EINK_CS, HIGH);
|
||||
digitalWrite(PIN_EINK_DC, HIGH);
|
||||
#else
|
||||
// Start HSPI
|
||||
hspi = new SPIClass(HSPI);
|
||||
hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // SCLK, MISO, MOSI, SS
|
||||
#endif
|
||||
// VExt already enabled in setup()
|
||||
// RTC GPIO hold disabled in setup()
|
||||
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if defined(USE_HUB75)
|
||||
|
||||
#include "HUB75Display.h"
|
||||
#include "TFTColorRegions.h"
|
||||
#include "TFTPalette.h"
|
||||
#include <ESP32-HUB75-MatrixPanel-I2S-DMA.h>
|
||||
#include <string.h>
|
||||
|
||||
HUB75Display::HUB75Display(uint8_t, int, int, OLEDDISPLAY_GEOMETRY, HW_I2C)
|
||||
{
|
||||
// The BaseUI treats the panel as a generic raw framebuffer (not an SSD1306
|
||||
// page layout). Geometry is fixed by the wired panel size.
|
||||
#if defined(SCREEN_ROTATE)
|
||||
setGeometry(GEOMETRY_RAWMODE, TFT_HEIGHT, TFT_WIDTH);
|
||||
#else
|
||||
setGeometry(GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT);
|
||||
#endif
|
||||
LOG_DEBUG("HUB75Display %dx%d", (int)TFT_WIDTH, (int)TFT_HEIGHT);
|
||||
}
|
||||
|
||||
HUB75Display::~HUB75Display()
|
||||
{
|
||||
if (matrix) {
|
||||
delete matrix;
|
||||
matrix = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// Bring up the matrix DMA driver from the wired HUB75 pins.
|
||||
bool HUB75Display::connect()
|
||||
{
|
||||
LOG_INFO("Do HUB75 init");
|
||||
|
||||
HUB75_I2S_CFG::i2s_pins pins = {HUB75_R1, HUB75_G1, HUB75_B1, HUB75_R2, HUB75_G2, HUB75_B2, HUB75_A,
|
||||
HUB75_B, HUB75_C, HUB75_D, HUB75_E, HUB75_LAT, HUB75_OE, HUB75_CLK};
|
||||
|
||||
HUB75_I2S_CFG cfg(TFT_WIDTH, TFT_HEIGHT, 1 /* chain length */, pins);
|
||||
|
||||
cfg.i2sspeed = HUB75_I2S_CFG::HZ_8M; // timing headroom (signal integrity)
|
||||
cfg.latch_blanking = 4; // clean row transitions
|
||||
cfg.clkphase = false; // inverted clock phase: fixes 1px skew of lower half
|
||||
cfg.double_buff = false; // single buffer: halves the internal-SRAM DMA
|
||||
// footprint. display() draws directly into the
|
||||
// live buffer with a dirty-diff
|
||||
|
||||
matrix = new MatrixPanel_I2S_DMA(cfg);
|
||||
bool ok = matrix->begin();
|
||||
if (!ok) {
|
||||
LOG_ERROR("HUB75 matrix->begin() failed (DMA buffer alloc?)");
|
||||
delete matrix;
|
||||
matrix = nullptr;
|
||||
return false;
|
||||
}
|
||||
matrix->setBrightness8(brightness);
|
||||
matrix->clearScreen();
|
||||
return true;
|
||||
}
|
||||
|
||||
void HUB75Display::display()
|
||||
{
|
||||
if (!matrix)
|
||||
return;
|
||||
|
||||
const uint16_t onNative = graphics::TFTPalette::White;
|
||||
const uint16_t offNative = graphics::getThemeBodyBg();
|
||||
|
||||
const uint16_t onBe = (uint16_t)((onNative >> 8) | (onNative << 8));
|
||||
const uint16_t offBe = (uint16_t)((offNative >> 8) | (offNative << 8));
|
||||
|
||||
bool forceFull = firstFrame || !haveThemeDefaults || onBe != lastOnBe || offBe != lastOffBe;
|
||||
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
const bool hasColorRegions = graphics::getTFTColorRegionCount() > 0;
|
||||
const uint32_t colorSig = graphics::getTFTColorFrameSignature();
|
||||
if (colorSig != lastColorSig)
|
||||
forceFull = true;
|
||||
#endif
|
||||
|
||||
for (uint16_t y = 0; y < displayHeight; y++) {
|
||||
const uint32_t yByteIndex = (y / 8) * displayWidth;
|
||||
const uint8_t yByteMask = (uint8_t)(1 << (y & 7));
|
||||
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
if (hasColorRegions)
|
||||
graphics::beginTFTColorRow((int16_t)y);
|
||||
#endif
|
||||
|
||||
for (uint16_t x = 0; x < displayWidth; x++) {
|
||||
const bool isset = (buffer[x + yByteIndex] & yByteMask) != 0;
|
||||
|
||||
// Skip pixels whose mono bit is unchanged (unless a full repaint is
|
||||
// forced). The panel is single-buffered, so untouched pixels persist.
|
||||
if (!forceFull && (((buffer_back[x + yByteIndex] & yByteMask) != 0) == isset))
|
||||
continue;
|
||||
|
||||
uint16_t be;
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
if (hasColorRegions)
|
||||
be = graphics::resolveTFTColorPixelRow((int16_t)x, isset, onBe, offBe);
|
||||
else
|
||||
be = isset ? onBe : offBe;
|
||||
#else
|
||||
be = isset ? onBe : offBe;
|
||||
#endif
|
||||
|
||||
const uint16_t c = (uint16_t)((be >> 8) | (be << 8)); // back to native RGB565
|
||||
const uint8_t r = (uint8_t)(((c >> 11) & 0x1F) << 3);
|
||||
const uint8_t g = (uint8_t)(((c >> 5) & 0x3F) << 2);
|
||||
const uint8_t b = (uint8_t)((c & 0x1F) << 3);
|
||||
matrix->drawPixelRGB888(x, y, r, g, b);
|
||||
}
|
||||
}
|
||||
|
||||
// Remember what the panel now shows so the next frame can diff against it.
|
||||
memcpy(buffer_back, buffer, displayBufferSize);
|
||||
|
||||
haveThemeDefaults = true;
|
||||
lastOnBe = onBe;
|
||||
lastOffBe = offBe;
|
||||
firstFrame = false;
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
lastColorSig = colorSig;
|
||||
// Regions are re-registered every frame by the renderers; clear so they
|
||||
// don't accumulate across frames.
|
||||
graphics::clearTFTColorRegions();
|
||||
#endif
|
||||
}
|
||||
|
||||
void HUB75Display::sendCommand(uint8_t com)
|
||||
{
|
||||
if (!matrix)
|
||||
return;
|
||||
|
||||
switch (com) {
|
||||
case DISPLAYON:
|
||||
matrix->setBrightness8(brightness);
|
||||
break;
|
||||
case DISPLAYOFF:
|
||||
matrix->setBrightness8(0);
|
||||
break;
|
||||
default:
|
||||
// Drop all other SSD1306 init/config commands - not meaningful for the matrix.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void HUB75Display::setDisplayBrightness(uint8_t _brightness)
|
||||
{
|
||||
brightness = _brightness;
|
||||
if (matrix)
|
||||
matrix->setBrightness8(brightness);
|
||||
}
|
||||
|
||||
#endif // USE_HUB75
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#if defined(USE_HUB75)
|
||||
|
||||
#include <OLEDDisplay.h>
|
||||
|
||||
#ifndef HUB75_BRIGHTNESS_DEFAULT
|
||||
#define HUB75_BRIGHTNESS_DEFAULT 180
|
||||
#endif
|
||||
|
||||
class MatrixPanel_I2S_DMA;
|
||||
|
||||
/**
|
||||
* A color display backend that drives a HUB75 RGB LED matrix panel from the
|
||||
* BaseUI color path.
|
||||
*/
|
||||
class HUB75Display : public OLEDDisplay
|
||||
{
|
||||
public:
|
||||
HUB75Display(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus);
|
||||
~HUB75Display();
|
||||
|
||||
// Write the buffer to the matrix (full-frame, region-aware color expansion).
|
||||
void display() override;
|
||||
|
||||
void setDisplayBrightness(uint8_t brightness);
|
||||
|
||||
protected:
|
||||
int getBufferOffset(void) override { return 0; }
|
||||
|
||||
void sendCommand(uint8_t com) override;
|
||||
|
||||
bool connect() override;
|
||||
|
||||
private:
|
||||
MatrixPanel_I2S_DMA *matrix = nullptr;
|
||||
uint8_t brightness = HUB75_BRIGHTNESS_DEFAULT;
|
||||
|
||||
bool firstFrame = true;
|
||||
bool haveThemeDefaults = false;
|
||||
uint16_t lastOnBe = 0;
|
||||
uint16_t lastOffBe = 0;
|
||||
uint32_t lastColorSig = 0;
|
||||
};
|
||||
|
||||
#endif // USE_HUB75
|
||||
+110
-9
@@ -29,6 +29,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#if HAS_SCREEN
|
||||
#include "EInkParallelDisplay.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#if defined(USE_HUB75)
|
||||
#include "graphics/HUB75Display.h" // ESP32 HUB75 (I2S-DMA)
|
||||
#endif
|
||||
#if defined(HAS_HUB75_NATIVE)
|
||||
#include "HUB75Native.h" // native/Portduino HUB75 (rpi-rgb-led-matrix), from variants/native/portduino
|
||||
#endif
|
||||
|
||||
#include "DisplayFormatters.h"
|
||||
#include "TimeFormatters.h"
|
||||
@@ -67,6 +73,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#include "mesh/Default.h"
|
||||
#include "mesh/generated/meshtastic/deviceonly.pb.h"
|
||||
#include "modules/ExternalNotificationModule.h"
|
||||
#if BASEUI_HAS_GAMES
|
||||
#include "modules/games/GamesModule.h"
|
||||
#endif
|
||||
#include "modules/WaypointModule.h"
|
||||
#include "sleep.h"
|
||||
#include "target_specific.h"
|
||||
@@ -99,7 +108,11 @@ namespace graphics
|
||||
#define COMPASS_ACTIVE_FRAMERATE 20
|
||||
|
||||
// DEBUG
|
||||
#if BASEUI_HAS_GAMES
|
||||
#define NUM_EXTRA_FRAMES 4 // text message, debug frame, and the always-present games frame
|
||||
#else
|
||||
#define NUM_EXTRA_FRAMES 3 // text message and debug frame
|
||||
#endif
|
||||
// if defined a pixel will blink to show redraws
|
||||
// #define SHOW_REDRAWS
|
||||
#define ASCII_BELL '\x07'
|
||||
@@ -343,7 +356,7 @@ void Screen::showNodePicker(const char *message, uint32_t durationMs, std::funct
|
||||
}
|
||||
|
||||
// Called to trigger a banner with custom message and duration
|
||||
void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits,
|
||||
void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, bool useBase16,
|
||||
std::function<void(uint32_t)> bannerCallback)
|
||||
{
|
||||
#ifdef USE_EINK
|
||||
@@ -356,7 +369,10 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
|
||||
NotificationRenderer::alertBannerCallback = bannerCallback;
|
||||
NotificationRenderer::pauseBanner = false;
|
||||
NotificationRenderer::curSelected = 0;
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::number_picker;
|
||||
if (useBase16)
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::hex_picker;
|
||||
else
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::number_picker;
|
||||
NotificationRenderer::numDigits = digits;
|
||||
NotificationRenderer::currentNumber = 0;
|
||||
|
||||
@@ -366,6 +382,43 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
|
||||
updateUiFrame(ui);
|
||||
}
|
||||
|
||||
// Called to trigger an arcade-style initials picker (see showNumberPicker for the sibling flow).
|
||||
void Screen::showAlphanumericPicker(const char *message, const char *initialText, uint32_t durationMs, uint8_t length,
|
||||
std::function<void(const std::string &)> bannerCallback)
|
||||
{
|
||||
#ifdef USE_EINK
|
||||
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
|
||||
#endif
|
||||
if (length >= sizeof(NotificationRenderer::alphanumericValue))
|
||||
length = sizeof(NotificationRenderer::alphanumericValue) - 1;
|
||||
|
||||
strncpy(NotificationRenderer::alertBannerMessage, message, 255);
|
||||
NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination
|
||||
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
|
||||
NotificationRenderer::textInputCallback = bannerCallback;
|
||||
NotificationRenderer::pauseBanner = false;
|
||||
NotificationRenderer::curSelected = 0;
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::alphanumeric_picker;
|
||||
NotificationRenderer::numDigits = length;
|
||||
|
||||
// Seed each position from initialText (uppercased & filtered to A-Z/0-9), defaulting to 'A'.
|
||||
const size_t seedLen = initialText ? strnlen(initialText, length) : 0;
|
||||
for (uint8_t i = 0; i < length; i++) {
|
||||
char c = (i < seedLen) ? initialText[i] : 'A';
|
||||
if (c >= 'a' && c <= 'z')
|
||||
c = static_cast<char>(c - 'a' + 'A');
|
||||
if (!((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')))
|
||||
c = 'A';
|
||||
NotificationRenderer::alphanumericValue[i] = c;
|
||||
}
|
||||
NotificationRenderer::alphanumericValue[length] = '\0';
|
||||
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, 2);
|
||||
ui->setTargetFPS(60);
|
||||
updateUiFrame(ui);
|
||||
}
|
||||
|
||||
void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||
std::function<void(const std::string &)> textCallback)
|
||||
{
|
||||
@@ -409,6 +462,17 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int
|
||||
pi.drawFrame(display, state, x, y);
|
||||
}
|
||||
|
||||
#if BASEUI_HAS_GAMES
|
||||
// The games frame is a dedicated, always-present frame (unlike generic module frames it is placed
|
||||
// at a fixed position right after home), so it draws through its own trampoline rather than the
|
||||
// moduleFrames lockstep used by drawModuleFrame.
|
||||
static void drawGamesFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
if (gamesModule)
|
||||
gamesModule->drawFrame(display, state, x, y);
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Given a recent lat/lon return a guess of the heading the user is walking on.
|
||||
*
|
||||
@@ -496,6 +560,8 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
|
||||
#else
|
||||
dispdev = new ST7796Spi(&SPI1, ST7796_RESET, ST7796_RS, ST7796_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT);
|
||||
#endif
|
||||
#elif defined(USE_HUB75)
|
||||
dispdev = new HUB75Display(address.address, -1, -1, GEOMETRY_RAWMODE, HW_I2C::I2C_ONE);
|
||||
#elif defined(USE_SSD1306)
|
||||
dispdev = new SSD1306Wire(address.address, -1, -1, geometry,
|
||||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||||
@@ -514,7 +580,17 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
|
||||
LOG_INFO("SSD1306 init success");
|
||||
}
|
||||
#elif ARCH_PORTDUINO
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
|
||||
// HUB75 RGB matrix (hzeller/rpi-rgb-led-matrix) is a BaseUI framebuffer panel, selected at
|
||||
// runtime via config.yaml Display: Panel: HUB75.
|
||||
if (portduino_config.displayPanel == hub75) {
|
||||
#if defined(HAS_HUB75_NATIVE)
|
||||
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!");
|
||||
#endif
|
||||
} else if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
if (portduino_config.displayPanel != no_screen) {
|
||||
LOG_DEBUG("Make TFTDisplay!");
|
||||
dispdev = new TFTDisplay(address.address, -1, -1, geometry,
|
||||
@@ -1301,6 +1377,15 @@ void Screen::setFrames(FrameFocus focus)
|
||||
indicatorIcons.push_back(icon_home);
|
||||
}
|
||||
|
||||
#if BASEUI_HAS_GAMES
|
||||
// Games frame: always present (even with no game running), positioned directly after home.
|
||||
if (gamesModule) {
|
||||
fsi.positions.games = numframes;
|
||||
normalFrames[numframes++] = drawGamesFrame;
|
||||
indicatorIcons.push_back(joystick_small);
|
||||
}
|
||||
#endif
|
||||
|
||||
fsi.positions.textMessage = numframes;
|
||||
normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame;
|
||||
indicatorIcons.push_back(icon_mail);
|
||||
@@ -2025,7 +2110,7 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
if (ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) {
|
||||
|
||||
if (event->inputEvent == INPUT_BROKER_UP) {
|
||||
if (messageStore.getMessages().empty()) {
|
||||
if (!messageStore.hasVisibleMessages()) {
|
||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
|
||||
} else {
|
||||
graphics::MessageRenderer::scrollUp();
|
||||
@@ -2035,7 +2120,7 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
}
|
||||
|
||||
if (event->inputEvent == INPUT_BROKER_DOWN) {
|
||||
if (messageStore.getMessages().empty()) {
|
||||
if (!messageStore.hasVisibleMessages()) {
|
||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
|
||||
} else {
|
||||
graphics::MessageRenderer::scrollDown();
|
||||
@@ -2073,14 +2158,20 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
if (module && module->interceptingKeyboardInput())
|
||||
inputIntercepted = true;
|
||||
}
|
||||
#if BASEUI_HAS_GAMES
|
||||
// The games frame isn't a moduleFrame, so check it explicitly: while a game is running it
|
||||
// owns the D-pad (turns/pause) and we must not switch frames or open menus underneath it.
|
||||
if (gamesModule && gamesModule->interceptingKeyboardInput())
|
||||
inputIntercepted = true;
|
||||
#endif
|
||||
|
||||
// If no modules are using the input, move between frames
|
||||
if (!inputIntercepted) {
|
||||
#if defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2
|
||||
bool handledEncoderScroll = false;
|
||||
const bool isTextMessageFrame = (framesetInfo.positions.textMessage != 255 &&
|
||||
this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage &&
|
||||
!messageStore.getMessages().empty());
|
||||
const bool isTextMessageFrame =
|
||||
(framesetInfo.positions.textMessage != 255 &&
|
||||
this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage && messageStore.hasVisibleMessages());
|
||||
if (isTextMessageFrame) {
|
||||
if (event->inputEvent == INPUT_BROKER_UP_LONG) {
|
||||
graphics::MessageRenderer::nudgeScroll(-1);
|
||||
@@ -2147,6 +2238,11 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
} else if (event->inputEvent == INPUT_BROKER_SELECT) {
|
||||
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
|
||||
menuHandler::homeBaseMenu();
|
||||
#if BASEUI_HAS_GAMES
|
||||
} else if (gamesModule && framesetInfo.positions.games != 255 &&
|
||||
this->ui->getUiState()->currentFrame == framesetInfo.positions.games) {
|
||||
gamesModule->launchGame(); // launch the game shown on the attract screen
|
||||
#endif
|
||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.system) {
|
||||
menuHandler::systemBaseMenu();
|
||||
#if HAS_GPS
|
||||
@@ -2158,7 +2254,7 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) {
|
||||
menuHandler::loraMenu();
|
||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) {
|
||||
if (!messageStore.getMessages().empty()) {
|
||||
if (messageStore.hasVisibleMessages()) {
|
||||
menuHandler::messageResponseMenu();
|
||||
} else {
|
||||
if (currentResolution == ScreenResolution::UltraLow) {
|
||||
@@ -2214,6 +2310,11 @@ bool Screen::isOverlayBannerShowing()
|
||||
return NotificationRenderer::isOverlayBannerShowing();
|
||||
}
|
||||
|
||||
bool Screen::isGamesFrameShown()
|
||||
{
|
||||
return framesetInfo.positions.games != 255 && ui && ui->getUiState()->currentFrame == framesetInfo.positions.games;
|
||||
}
|
||||
|
||||
} // namespace graphics
|
||||
|
||||
#else
|
||||
|
||||
+15
-1
@@ -26,6 +26,9 @@ enum notificationTypeEnum {
|
||||
// LOCKED frame. Without this, a first-pair on a locked device cannot
|
||||
// complete because the PIN never renders.
|
||||
pairing_pin,
|
||||
// Arcade-style initials entry: like number_picker/hex_picker, but each position cycles
|
||||
// through A-Z and 0-9. The assembled string is returned via a text (std::string) callback.
|
||||
alphanumeric_picker,
|
||||
};
|
||||
|
||||
struct BannerOverlayOptions {
|
||||
@@ -284,6 +287,10 @@ class Screen : public concurrency::OSThread
|
||||
|
||||
bool isOverlayBannerShowing();
|
||||
|
||||
// True if the always-present games frame is the one currently on screen. Lets the games module
|
||||
// ignore D-pad input when the player has navigated to a different frame.
|
||||
bool isGamesFrameShown();
|
||||
|
||||
bool isScreenOn() { return screenOn; }
|
||||
|
||||
// Stores the last 4 of our hardware ID, to make finding the device for pairing easier
|
||||
@@ -344,7 +351,13 @@ class Screen : public concurrency::OSThread
|
||||
void showOverlayBanner(BannerOverlayOptions);
|
||||
|
||||
void showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback);
|
||||
void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function<void(uint32_t)> bannerCallback);
|
||||
void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, bool useBase16,
|
||||
std::function<void(uint32_t)> bannerCallback);
|
||||
// Arcade-style initials entry. `length` positions each cycle A-Z/0-9 (UP/DOWN), LEFT/RIGHT
|
||||
// moves the cursor, SELECT advances; the assembled string is delivered to `bannerCallback`.
|
||||
// `initialText` pre-seeds the positions (uppercased & filtered), defaulting to 'A'.
|
||||
void showAlphanumericPicker(const char *message, const char *initialText, uint32_t durationMs, uint8_t length,
|
||||
std::function<void(const std::string &)> bannerCallback);
|
||||
void showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||
std::function<void(const std::string &)> textCallback);
|
||||
|
||||
@@ -734,6 +747,7 @@ class Screen : public concurrency::OSThread
|
||||
uint8_t system = 255;
|
||||
uint8_t gps = 255;
|
||||
uint8_t home = 255;
|
||||
uint8_t games = 255;
|
||||
uint8_t textMessage = 255;
|
||||
uint8_t nodelist_nodes = 255;
|
||||
uint8_t nodelist_location = 255;
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "Power.h"
|
||||
#include "RTC.h"
|
||||
#include "draw/NodeListRenderer.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/TFTColorRegions.h"
|
||||
@@ -147,8 +147,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
// Transparent clock headers should inherit whatever body off-color is
|
||||
// already active under the header (important for light/inverted themes).
|
||||
const uint16_t transparentBgColor = resolveTFTOffColorAt(0, headerHeight + 1, getThemeBodyBg());
|
||||
setAndRegisterTFTColorRole(TFTColorRole::HeaderBackground, transparentBgColor, transparentBgColor, 0, 0, screenW,
|
||||
headerHeight);
|
||||
// Intentionally skip the HeaderBackground region, as small screens draw the clock in the unused space in this region,
|
||||
// and the transparent call was erasing segments from the clock
|
||||
setTFTColorRole(TFTColorRole::HeaderTitle, headerTitleColorForRole, transparentBgColor);
|
||||
setTFTColorRole(TFTColorRole::HeaderStatus, headerStatusColor, transparentBgColor);
|
||||
} else if (useInvertedHeaderStyle) {
|
||||
|
||||
@@ -16,8 +16,9 @@ struct TFTColorRegion {
|
||||
// Required by ST7789 driver: it scans until the first disabled entry.
|
||||
bool enabled = false;
|
||||
};
|
||||
|
||||
#ifndef MAX_TFT_COLOR_REGIONS
|
||||
static constexpr size_t MAX_TFT_COLOR_REGIONS = 48;
|
||||
#endif
|
||||
extern TFTColorRegion colorRegions[MAX_TFT_COLOR_REGIONS];
|
||||
|
||||
enum class TFTColorRole : uint8_t {
|
||||
@@ -39,7 +40,7 @@ enum class TFTColorRole : uint8_t {
|
||||
Count
|
||||
};
|
||||
|
||||
#if HAS_TFT || defined(HAS_SPI_TFT)
|
||||
#if HAS_TFT || defined(HAS_SPI_TFT) || defined(HAS_HUB75_NATIVE)
|
||||
#define GRAPHICS_TFT_COLORING_ENABLED 1
|
||||
#else
|
||||
#define GRAPHICS_TFT_COLORING_ENABLED 0
|
||||
|
||||
@@ -121,7 +121,6 @@ static void rak14014_tpIntHandle(void)
|
||||
|
||||
#elif defined(HACKADAY_COMMUNICATOR)
|
||||
#include <Arduino_GFX_Library.h>
|
||||
Arduino_DataBus *bus = nullptr;
|
||||
Arduino_GFX *tft = nullptr;
|
||||
|
||||
#elif defined(ST72xx_DE)
|
||||
@@ -1601,17 +1600,21 @@ bool TFTDisplay::connect()
|
||||
{
|
||||
concurrency::LockGuard g(spiLock);
|
||||
LOG_INFO("Do TFT init");
|
||||
// connect() re-runs on every display wake on variants whose handleSetOn() re-inits
|
||||
// the UI (see the gates in Screen::handleSetOn); construct the driver exactly once.
|
||||
if (!tft) {
|
||||
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) || defined(HELTEC_MESH_NODE_T1)
|
||||
tft = new TFT_eSPI;
|
||||
tft = new TFT_eSPI;
|
||||
#elif defined(HACKADAY_COMMUNICATOR)
|
||||
bus = new Arduino_ESP32SPI(TFT_DC, TFT_CS, 38 /* SCK */, 21 /* MOSI */, GFX_NOT_DEFINED /* MISO */, HSPI /* spi_num */);
|
||||
tft = new Arduino_NV3007(bus, 40, 0 /* rotation */, false /* IPS */, 142 /* width */, 428 /* height */, 12 /* col offset 1 */,
|
||||
0 /* row offset 1 */, 14 /* col offset 2 */, 0 /* row offset 2 */, nv3007_279_init_operations,
|
||||
sizeof(nv3007_279_init_operations));
|
||||
|
||||
Arduino_DataBus *bus =
|
||||
new Arduino_ESP32SPI(TFT_DC, TFT_CS, 38 /* SCK */, 21 /* MOSI */, GFX_NOT_DEFINED /* MISO */, HSPI /* spi_num */);
|
||||
tft = new Arduino_NV3007(bus, 40, 0 /* rotation */, false /* IPS */, 142 /* width */, 428 /* height */,
|
||||
12 /* col offset 1 */, 0 /* row offset 1 */, 14 /* col offset 2 */, 0 /* row offset 2 */,
|
||||
nv3007_279_init_operations, sizeof(nv3007_279_init_operations));
|
||||
#else
|
||||
tft = new LGFX;
|
||||
tft = new LGFX;
|
||||
#endif
|
||||
}
|
||||
|
||||
backlightEnable->set(true);
|
||||
LOG_INFO("Power to TFT Backlight");
|
||||
|
||||
@@ -743,17 +743,18 @@ void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int1
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
int line = 1;
|
||||
int scale = 1;
|
||||
int iconX = SCREEN_WIDTH - chirpy_width - (chirpy_width / 3);
|
||||
int iconY = (SCREEN_HEIGHT - chirpy_height) / 2;
|
||||
int textX_offset = 10;
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
textX_offset = textX_offset * 4;
|
||||
const int scale = 2;
|
||||
scale = 2;
|
||||
iconX = SCREEN_WIDTH - (chirpy_width * 2) - ((chirpy_width * 2) / 3);
|
||||
iconY = (SCREEN_HEIGHT - (chirpy_height * 2)) / 2;
|
||||
const int bytesPerRow = (chirpy_width + 7) / 8;
|
||||
|
||||
for (int yy = 0; yy < chirpy_height; ++yy) {
|
||||
iconX = SCREEN_WIDTH - (chirpy_width * 2) - ((chirpy_width * 2) / 3);
|
||||
iconY = (SCREEN_HEIGHT - (chirpy_height * 2)) / 2;
|
||||
const uint8_t *rowPtr = chirpy + yy * bytesPerRow;
|
||||
for (int xx = 0; xx < chirpy_width; ++xx) {
|
||||
const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3));
|
||||
@@ -767,6 +768,19 @@ void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int1
|
||||
display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy);
|
||||
}
|
||||
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
// Colour Chirpy on colour displays. The glyph is a filled head silhouette whose eyes are holes
|
||||
// (off pixels), so two stacked regions render the proper mascot without a second bitmap:
|
||||
// A) whole glyph -> green body / frame / legs
|
||||
// B) the eye band -> black face, with the eye holes turning white via the region's off-colour
|
||||
// Start the face band one column in from the head's left edge (col 6) so that edge stays green,
|
||||
// matching the green column already left on the right edge (col 31).
|
||||
graphics::registerTFTColorRegionDirect(iconX, iconY, chirpy_width * scale, chirpy_height * scale,
|
||||
graphics::TFTPalette::MeshtasticGreen, graphics::getThemeBodyBg());
|
||||
graphics::registerTFTColorRegionDirect(iconX + 7 * scale, iconY + 12 * scale, 24 * scale, 16 * scale,
|
||||
graphics::TFTPalette::Black, graphics::TFTPalette::White);
|
||||
#endif
|
||||
|
||||
int textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("Hello") / 2);
|
||||
display->drawString(textX, getTextPositions(display)[line++], "Hello");
|
||||
textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("World!") / 2);
|
||||
|
||||
@@ -70,12 +70,15 @@ const StoredMessage *getNewestMessageForActiveThread()
|
||||
const uint32_t peer = graphics::MessageRenderer::getThreadPeer();
|
||||
const uint32_t localNode = nodeDB->getNodeNum();
|
||||
|
||||
if (mode == graphics::MessageRenderer::ThreadMode::ALL) {
|
||||
return &messages.back();
|
||||
}
|
||||
|
||||
for (auto it = messages.rbegin(); it != messages.rend(); ++it) {
|
||||
const StoredMessage &m = *it;
|
||||
if (!messageStore.isMessageVisible(m)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mode == graphics::MessageRenderer::ThreadMode::ALL) {
|
||||
return &m;
|
||||
}
|
||||
|
||||
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
|
||||
if (m.type == MessageType::BROADCAST && static_cast<int>(m.channelIndex) == channel) {
|
||||
@@ -242,7 +245,6 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||
{"TH", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_TH},
|
||||
{"LORA_24", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_LORA_24},
|
||||
{"UA_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_UA_433},
|
||||
{"UA_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_UA_868},
|
||||
{"MY_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_MY_433},
|
||||
{"MY_919", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_MY_919},
|
||||
{"SG_923", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_SG_923},
|
||||
@@ -2323,8 +2325,10 @@ void menuHandler::testMenu()
|
||||
|
||||
void menuHandler::numberTest()
|
||||
{
|
||||
screen->showNumberPicker("Pick a number\n ", 30000, 4,
|
||||
[](int number_picked) -> void { LOG_WARN("Nodenum: %u", number_picked); });
|
||||
screen->showNumberPicker("Verify Nodenum:\n ", 30000, 8, true, [](uint32_t number_picked) -> void {
|
||||
LOG_DEBUG("Nodenum: 0x%08x", number_picked);
|
||||
keyVerificationModule->sendInitialRequest(number_picked);
|
||||
});
|
||||
}
|
||||
|
||||
void menuHandler::wifiBaseMenu()
|
||||
@@ -2508,8 +2512,7 @@ void menuHandler::keyVerificationFinalPrompt()
|
||||
options.notificationType = graphics::notificationTypeEnum::selection_picker;
|
||||
options.bannerCallback = [=](int selected) {
|
||||
if (selected == 1) {
|
||||
auto remoteNodePtr = nodeDB->getMeshNode(keyVerificationModule->getCurrentRemoteNode());
|
||||
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||
keyVerificationModule->commitVerifiedRemoteNode();
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(options);
|
||||
|
||||
@@ -403,6 +403,8 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
// Filter messages based on thread mode
|
||||
std::deque<StoredMessage> filtered;
|
||||
for (const auto &m : messageStore.getLiveMessages()) {
|
||||
if (!messageStore.isMessageVisible(m))
|
||||
continue;
|
||||
bool include = false;
|
||||
switch (currentMode) {
|
||||
case ThreadMode::ALL:
|
||||
|
||||
@@ -54,6 +54,7 @@ bool NotificationRenderer::pauseBanner = false;
|
||||
notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none;
|
||||
uint32_t NotificationRenderer::numDigits = 0;
|
||||
uint32_t NotificationRenderer::currentNumber = 0;
|
||||
char NotificationRenderer::alphanumericValue[16] = {0};
|
||||
VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr;
|
||||
std::function<void(const std::string &)> NotificationRenderer::textInputCallback = nullptr;
|
||||
|
||||
@@ -277,6 +278,9 @@ void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayU
|
||||
case notificationTypeEnum::hex_picker:
|
||||
drawHexPicker(display, state);
|
||||
break;
|
||||
case notificationTypeEnum::alphanumeric_picker:
|
||||
drawAlphanumericPicker(display, state);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,6 +466,96 @@ void NotificationRenderer::drawHexPicker(OLEDDisplay *display, OLEDDisplayUiStat
|
||||
drawNotificationBox(display, state, linePointers, totalLines, 0);
|
||||
}
|
||||
|
||||
// Arcade-style initials entry. Mirrors drawHexPicker's cursor/confirm flow, but each position
|
||||
// holds a character from ALPHANUMERIC_CHARS (cycled with UP/DOWN) instead of a packed digit, and
|
||||
// the assembled string is returned through textInputCallback.
|
||||
void NotificationRenderer::drawAlphanumericPicker(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||
{
|
||||
static const char ALPHANUMERIC_CHARS[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
constexpr int ALPHANUMERIC_COUNT = sizeof(ALPHANUMERIC_CHARS) - 1; // exclude the NUL
|
||||
|
||||
const char *lineStarts[MAX_LINES + 1] = {0};
|
||||
uint16_t lineCount = 0;
|
||||
|
||||
// Parse lines (identical to the number/hex pickers)
|
||||
char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage));
|
||||
lineStarts[lineCount] = alertBannerMessage;
|
||||
while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) {
|
||||
lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n');
|
||||
if (lineStarts[lineCount + 1][0] == '\n')
|
||||
lineStarts[lineCount + 1] += 1;
|
||||
lineCount++;
|
||||
}
|
||||
|
||||
auto alphaIndex = [&](char c) -> int {
|
||||
for (int i = 0; i < ALPHANUMERIC_COUNT; i++)
|
||||
if (ALPHANUMERIC_CHARS[i] == c)
|
||||
return i;
|
||||
return 0;
|
||||
};
|
||||
|
||||
// Handle input
|
||||
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS ||
|
||||
inEvent.inputEvent == INPUT_BROKER_UP_LONG) {
|
||||
int idx = (alphaIndex(alphanumericValue[curSelected]) + 1) % ALPHANUMERIC_COUNT;
|
||||
alphanumericValue[curSelected] = ALPHANUMERIC_CHARS[idx];
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS ||
|
||||
inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
|
||||
int idx = (alphaIndex(alphanumericValue[curSelected]) + ALPHANUMERIC_COUNT - 1) % ALPHANUMERIC_COUNT;
|
||||
alphanumericValue[curSelected] = ALPHANUMERIC_CHARS[idx];
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_ANYKEY) {
|
||||
char k = inEvent.kbchar;
|
||||
if (k >= 'a' && k <= 'z')
|
||||
k = static_cast<char>(k - 'a' + 'A');
|
||||
if ((k >= 'A' && k <= 'Z') || (k >= '0' && k <= '9')) { // direct keyboard entry
|
||||
alphanumericValue[curSelected] = k;
|
||||
curSelected++;
|
||||
}
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT || inEvent.inputEvent == INPUT_BROKER_RIGHT) {
|
||||
curSelected++;
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_LEFT) {
|
||||
curSelected--;
|
||||
} else if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) &&
|
||||
alertBannerUntil != 0) {
|
||||
resetBanner();
|
||||
return;
|
||||
}
|
||||
|
||||
if (curSelected < 0)
|
||||
curSelected = 0;
|
||||
if (curSelected == static_cast<int8_t>(numDigits)) {
|
||||
auto callback = textInputCallback; // capture before clearing to avoid re-entrancy surprises
|
||||
std::string result(alphanumericValue, numDigits);
|
||||
textInputCallback = nullptr;
|
||||
resetBanner();
|
||||
if (callback)
|
||||
callback(result);
|
||||
return;
|
||||
}
|
||||
|
||||
inEvent.inputEvent = INPUT_BROKER_NONE;
|
||||
if (alertBannerMessage[0] == '\0')
|
||||
return;
|
||||
|
||||
uint16_t totalLines = lineCount + 2;
|
||||
const char *linePointers[totalLines + 1] = {0}; // this is sort of a dynamic allocation
|
||||
|
||||
for (uint16_t i = 0; i < lineCount; i++) {
|
||||
linePointers[i] = lineStarts[i];
|
||||
}
|
||||
std::string chars = " ";
|
||||
std::string arrowPointer = " ";
|
||||
for (uint16_t i = 0; i < numDigits; i++) {
|
||||
chars += std::string(1, alphanumericValue[i]) + " ";
|
||||
arrowPointer += (curSelected == static_cast<int8_t>(i)) ? "^ " : "_ ";
|
||||
}
|
||||
|
||||
linePointers[lineCount++] = chars.c_str();
|
||||
linePointers[lineCount++] = arrowPointer.c_str();
|
||||
|
||||
drawNotificationBox(display, state, linePointers, totalLines, 0);
|
||||
}
|
||||
|
||||
void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||
{
|
||||
static uint32_t selectedNodenum = 0;
|
||||
@@ -1038,10 +1132,9 @@ void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiStat
|
||||
// Cancel virtual keyboard - call callback with empty string to indicate timeout
|
||||
auto callback = textInputCallback; // Store callback before clearing
|
||||
|
||||
// Clean up first to prevent re-entry
|
||||
delete virtualKeyboard;
|
||||
virtualKeyboard = nullptr;
|
||||
textInputCallback = nullptr;
|
||||
// Clean up first to prevent re-entry. The keyboard belongs to OnScreenKeyboardModule; only stop()
|
||||
// may free it, and it clears virtualKeyboard/textInputCallback for us.
|
||||
OnScreenKeyboardModule::instance().stop(false);
|
||||
resetBanner();
|
||||
|
||||
// Call callback after cleanup
|
||||
@@ -1060,9 +1153,7 @@ void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiStat
|
||||
bool handled = OnScreenKeyboardModule::processVirtualKeyboardInput(inEvent, virtualKeyboard);
|
||||
if (!handled && inEvent.inputEvent == INPUT_BROKER_CANCEL) {
|
||||
auto callback = textInputCallback;
|
||||
delete virtualKeyboard;
|
||||
virtualKeyboard = nullptr;
|
||||
textInputCallback = nullptr;
|
||||
OnScreenKeyboardModule::instance().stop(false); // sole owner of the keyboard; also clears our aliases
|
||||
resetBanner();
|
||||
if (callback) {
|
||||
callback("");
|
||||
|
||||
@@ -26,6 +26,7 @@ class NotificationRenderer
|
||||
static std::function<void(int)> alertBannerCallback;
|
||||
static uint32_t numDigits;
|
||||
static uint32_t currentNumber;
|
||||
static char alphanumericValue[16]; // working buffer for the alphanumeric_picker
|
||||
static VirtualKeyboard *virtualKeyboard;
|
||||
static std::function<void(const std::string &)> textInputCallback;
|
||||
|
||||
@@ -43,6 +44,7 @@ class NotificationRenderer
|
||||
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawHexPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawAlphanumericPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1],
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||
#include "modules/StatusMessageModule.h"
|
||||
#endif
|
||||
#if BASEUI_HAS_GAMES
|
||||
#include "modules/games/GamesModule.h"
|
||||
#endif
|
||||
#include "UIRenderer.h"
|
||||
#include "airtime.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
@@ -21,8 +24,8 @@
|
||||
#include "main.h"
|
||||
#include "target_specific.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#include <RTC.h>
|
||||
#include <cstring>
|
||||
#include <gps/RTC.h>
|
||||
|
||||
// External variables
|
||||
extern graphics::Screen *screen;
|
||||
@@ -844,6 +847,7 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
|
||||
return -6.0f;
|
||||
case PRESET(MEDIUM_SLOW):
|
||||
case PRESET(MEDIUM_FAST):
|
||||
case PRESET(MEDIUM_TURBO):
|
||||
return -5.5f;
|
||||
case PRESET(SHORT_SLOW):
|
||||
case PRESET(SHORT_FAST):
|
||||
@@ -1816,6 +1820,13 @@ constexpr uint32_t ICON_DISPLAY_DURATION_MS = 2000;
|
||||
// cppcheck-suppress constParameterPointer; signature must match OverlayCallback typedef from OLEDDisplayUi library
|
||||
void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||
{
|
||||
#if BASEUI_HAS_GAMES
|
||||
// Hide the navigation bar while a game owns the screen (the attract screen doesn't intercept,
|
||||
// so the nav bar stays visible there).
|
||||
if (gamesModule && gamesModule->interceptingKeyboardInput())
|
||||
return;
|
||||
#endif
|
||||
|
||||
uint8_t frameToHighlight = state->currentFrame;
|
||||
if (state->frameState == IN_TRANSITION && state->transitionFrameTarget < screen->indicatorIcons.size()) {
|
||||
frameToHighlight = state->transitionFrameTarget;
|
||||
|
||||
@@ -319,6 +319,46 @@ const uint8_t chirpy_small[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f};
|
||||
#define connection_icon_height 5
|
||||
const uint8_t connection_icon[] = {0x36, 0x41, 0x5D, 0x41, 0x36};
|
||||
|
||||
// Joystick icon (16x16): a round knob on a shaft over an elliptical base. Drawn on the Snake
|
||||
// attract screen as a "use the D-pad" controls hint.
|
||||
#define joystick_width 16
|
||||
#define joystick_height 16
|
||||
static const uint8_t joystick[] PROGMEM = {0xC0, 0x03, 0xE0, 0x07, 0xF0, 0x0F, 0xF0, 0x0F, 0xE0, 0x07, 0xC0,
|
||||
0x03, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0xF8, 0x1F,
|
||||
0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0xF8, 0x1F};
|
||||
|
||||
// 8x8 joystick, for the navigation bar (which draws 8x8 glyphs, scaled up on hi-res displays).
|
||||
#define joystick_small_width 8
|
||||
#define joystick_small_height 8
|
||||
static const uint8_t joystick_small[] PROGMEM = {0x18, 0x3C, 0x3C, 0x18, 0x7E, 0x99, 0xC3, 0x7E};
|
||||
|
||||
#define snake_width 16
|
||||
#define snake_height 16
|
||||
static const uint8_t snake[] PROGMEM = {0x80, 0x1F, 0x40, 0x20, 0x20, 0x48, 0x20, 0x40, 0xA0, 0x44, 0x20,
|
||||
0x39, 0x40, 0xDE, 0x86, 0x44, 0x0A, 0x19, 0x32, 0x22, 0xC4, 0x47,
|
||||
0x1C, 0x4D, 0x6A, 0x4A, 0xFA, 0x47, 0x04, 0x20, 0xF8, 0x1F};
|
||||
|
||||
#define tetris_width 16
|
||||
#define tetris_height 16
|
||||
static const uint8_t tetris[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x01, 0x80, 0xFD, 0xBF, 0xFD,
|
||||
0xBF, 0x81, 0x81, 0xBF, 0xFD, 0xA0, 0x05, 0xA0, 0x05, 0xA0, 0x05,
|
||||
0x20, 0x04, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
#define breakout_width 16
|
||||
#define breakout_height 16
|
||||
// Three staggered brick courses, a ball, and a paddle. XBM: 2 bytes/row, LSB = leftmost pixel.
|
||||
static const uint8_t breakout[] PROGMEM = {0x00, 0x00, 0x77, 0x77, 0x77, 0x77, 0x00, 0x00, 0xDD, 0xDD, 0xDD,
|
||||
0xDD, 0x00, 0x00, 0x77, 0x77, 0x77, 0x77, 0x00, 0x00, 0x80, 0x01,
|
||||
0x80, 0x01, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0xF0, 0x0F};
|
||||
|
||||
// Tiny Chirpy runner sprite: square head with two pill eyes + mouth, antenna, and long legs.
|
||||
// XBM, 2 bytes/row, LSB = leftmost pixel. Used by the Chirpy Runner game.
|
||||
#define chirpy_run_width 12
|
||||
#define chirpy_run_height 16
|
||||
static const uint8_t chirpy_run[] PROGMEM = {0x40, 0x00, 0x20, 0x00, 0x40, 0x00, 0xfc, 0x03, 0x04, 0x02, 0x94,
|
||||
0x02, 0x94, 0x02, 0x94, 0x02, 0x04, 0x02, 0xf4, 0x02, 0xfc, 0x03,
|
||||
0x90, 0x00, 0x90, 0x00, 0x90, 0x00, 0x90, 0x00, 0x08, 0x01};
|
||||
|
||||
#ifdef OLED_TINY
|
||||
#include "img/icon_small.xbm"
|
||||
#else
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
#include "main.h"
|
||||
|
||||
#include "RTC.h"
|
||||
#include "gps/RTC.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "RTC.h"
|
||||
#include "gps/RTC.h"
|
||||
|
||||
#include "GeoCoord.h"
|
||||
#include "NodeDB.h"
|
||||
|
||||
@@ -57,7 +57,6 @@ enum MenuAction {
|
||||
SET_REGION_TH,
|
||||
SET_REGION_LORA_24,
|
||||
SET_REGION_UA_433,
|
||||
SET_REGION_UA_868,
|
||||
SET_REGION_MY_433,
|
||||
SET_REGION_MY_919,
|
||||
SET_REGION_SG_923,
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
#include "MeshService.h"
|
||||
#include "MessageStore.h"
|
||||
#include "Power.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "airtime.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h"
|
||||
#include "graphics/niche/Utils/FlashData.h"
|
||||
#include "main.h"
|
||||
@@ -835,10 +835,6 @@ void InkHUD::MenuApplet::execute(MenuItem item)
|
||||
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_UA_433);
|
||||
break;
|
||||
|
||||
case SET_REGION_UA_868:
|
||||
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_UA_868);
|
||||
break;
|
||||
|
||||
case SET_REGION_MY_433:
|
||||
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_MY_433);
|
||||
break;
|
||||
@@ -1735,7 +1731,6 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
items.push_back(MenuItem("TH", MenuAction::SET_REGION_TH, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("LoRa 2.4", MenuAction::SET_REGION_LORA_24, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("UA 433", MenuAction::SET_REGION_UA_433, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("UA 868", MenuAction::SET_REGION_UA_868, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("MY 433", MenuAction::SET_REGION_MY_433, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("MY 919", MenuAction::SET_REGION_MY_919, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("SG 923", MenuAction::SET_REGION_SG_923, MenuPage::EXIT));
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#include "meshUtils.h"
|
||||
#include "modules/TextMessageModule.h"
|
||||
|
||||
#include "RTC.h"
|
||||
#include "gps/RTC.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
@@ -234,7 +234,8 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila
|
||||
// Pick source of message
|
||||
const StoredMessage *message =
|
||||
msgIsBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm;
|
||||
|
||||
if (!message->sender || !messageStore.isMessageVisible(*message))
|
||||
return parse(text);
|
||||
// Find info about the sender
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(message->sender);
|
||||
|
||||
@@ -270,4 +271,4 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila
|
||||
return parse(text);
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -46,7 +46,7 @@ void InkHUD::AllMessageApplet::onRender(bool full)
|
||||
message = &latestMessage->dm;
|
||||
|
||||
// Short circuit: no text message
|
||||
if (!message->sender) {
|
||||
if (!message->sender || !messageStore.isMessageVisible(*message)) {
|
||||
printAt(X(0.5), Y(0.5), "No Message", CENTER, MIDDLE);
|
||||
return;
|
||||
}
|
||||
@@ -138,4 +138,4 @@ bool InkHUD::AllMessageApplet::approveNotification(Notification &n)
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -42,7 +42,7 @@ int InkHUD::DMApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p)
|
||||
void InkHUD::DMApplet::onRender(bool full)
|
||||
{
|
||||
// Abort if no text message
|
||||
if (!latestMessage->dm.sender) {
|
||||
if (!latestMessage->dm.sender || !messageStore.isMessageVisible(latestMessage->dm)) {
|
||||
printAt(X(0.5), Y(0.5), "No DMs", CENTER, MIDDLE);
|
||||
return;
|
||||
}
|
||||
@@ -131,4 +131,4 @@ bool InkHUD::DMApplet::approveNotification(Notification &n)
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "RTC.h"
|
||||
#include "gps/RTC.h"
|
||||
|
||||
#include "gps/GeoCoord.h"
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include "./RecentsListApplet.h"
|
||||
|
||||
#include "RTC.h"
|
||||
#include "gps/RTC.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include "./ThreadedMessageApplet.h"
|
||||
|
||||
#include "RTC.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "mesh/NodeDB.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
@@ -191,7 +191,8 @@ ProcessMessage InkHUD::ThreadedMessageApplet::handleReceived(const meshtastic_Me
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
// Store in the global messageStore - this handles sender, timestamp, channel, text, and ack status
|
||||
messageStore.addFromPacket(mp);
|
||||
if (!messageStore.tryAddFromPacket(mp))
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
// If this was an incoming message, suggest that our applet becomes foreground, if permitted
|
||||
if (getFrom(&mp) != nodeDB->getNodeNum())
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
#include "MessageStore.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "buzz.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "modules/ExternalNotificationModule.h"
|
||||
#include "modules/TextMessageModule.h"
|
||||
#include "sleep.h"
|
||||
@@ -534,13 +534,19 @@ int InkHUD::Events::onReceiveTextMessage(const meshtastic_MeshPacket *packet)
|
||||
if (getFrom(packet) == nodeDB->getNodeNum())
|
||||
return 0;
|
||||
|
||||
if (!messageStore.shouldStorePacket(*packet))
|
||||
return 0;
|
||||
|
||||
bool isBroadcastMsg = isBroadcast(packet->to);
|
||||
inkhud->persistence->latestMessage.wasBroadcast = isBroadcastMsg;
|
||||
|
||||
if (!isBroadcastMsg) {
|
||||
// DMs never pass through ThreadedMessageApplet, so add them to the global store here
|
||||
// so they survive reboots. Derive the latestMessage cache entry from the stored result.
|
||||
inkhud->persistence->latestMessage.dm = messageStore.addFromPacket(*packet);
|
||||
const StoredMessage *stored = messageStore.tryAddFromPacket(*packet);
|
||||
if (!stored)
|
||||
return 0;
|
||||
inkhud->persistence->latestMessage.dm = *stored;
|
||||
} else {
|
||||
// Broadcasts are added to the global store by ThreadedMessageApplet::handleReceived().
|
||||
// Here we only update the latestMessage cache used by AllMessageApplet / NotificationApplet.
|
||||
|
||||
@@ -26,6 +26,10 @@ void InkHUD::Persistence::loadLatestMessage()
|
||||
|
||||
int lastBroadcastPos = -1, lastDMPos = -1, pos = 0;
|
||||
for (const StoredMessage &m : messageStore.getLiveMessages()) {
|
||||
if (!messageStore.isMessageVisible(m)) {
|
||||
pos++;
|
||||
continue;
|
||||
}
|
||||
if (m.type == MessageType::BROADCAST) {
|
||||
latestMessage.broadcast = m;
|
||||
lastBroadcastPos = pos;
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
[inkhud]
|
||||
build_src_filter =
|
||||
build_src_filter =
|
||||
+<graphics/niche/>; Include the nicheGraphics directory
|
||||
build_flags =
|
||||
-<graphics/draw/> ; BaseUI renderer stack is disabled for InkHUD
|
||||
-<graphics/fonts/> ; InkHUD uses GFX_Root fonts instead of BaseUI font tables
|
||||
-<graphics/EInkDisplay2.cpp> ; InkHUD drives e-ink through niche drivers
|
||||
-<graphics/EInkDynamicDisplay.cpp>
|
||||
-<graphics/EInkParallelDisplay.cpp>
|
||||
-<graphics/EmoteRenderer.cpp>
|
||||
-<graphics/emotes.cpp>
|
||||
-<graphics/Panel_sdl.cpp>
|
||||
-<graphics/ScreenGlobals.cpp>
|
||||
-<graphics/SharedUIDisplay.cpp>
|
||||
-<graphics/TFTColorRegions.cpp>
|
||||
-<graphics/TFTDisplay.cpp>
|
||||
-<graphics/tftSetup.cpp>
|
||||
-<graphics/TimeFormatters.cpp>
|
||||
-<graphics/VirtualKeyboard.cpp>
|
||||
build_flags =
|
||||
-D MESHTASTIC_INCLUDE_NICHE_GRAPHICS ; Use NicheGraphics
|
||||
-D MESHTASTIC_INCLUDE_INKHUD ; Use InkHUD (a NicheGraphics UI)
|
||||
-D MESHTASTIC_EXCLUDE_SCREEN ; Suppress default Screen class
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#if ARCH_PORTDUINO
|
||||
#include "input/LinuxInputImpl.h"
|
||||
#include "input/LinuxJoystick.h"
|
||||
#include "input/SeesawRotary.h"
|
||||
#include "platform/portduino/PortduinoGlue.h"
|
||||
#endif
|
||||
@@ -430,6 +431,9 @@ void InputBroker::Init()
|
||||
// Linux evdev keyboard input only - macOS has no <linux/input.h>.
|
||||
aLinuxInputImpl = new LinuxInputImpl();
|
||||
aLinuxInputImpl->init();
|
||||
// Linux evdev gamepad/joystick input (D-pad + confirm/cancel buttons).
|
||||
aLinuxJoystick = new LinuxJoystick("LinuxJoystick");
|
||||
aLinuxJoystick->init();
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -25,8 +25,14 @@ LinuxInput::LinuxInput(const char *name) : concurrency::OSThread(name)
|
||||
|
||||
void LinuxInput::deInit()
|
||||
{
|
||||
// reboot() on native does execv() in-place, which only closes O_CLOEXEC fds.
|
||||
// Close both descriptors here so we don't leak one per restart.
|
||||
if (fd >= 0)
|
||||
close(fd);
|
||||
if (epollfd >= 0)
|
||||
close(epollfd);
|
||||
fd = -1;
|
||||
epollfd = -1;
|
||||
}
|
||||
|
||||
int32_t LinuxInput::runOnce()
|
||||
@@ -35,14 +41,14 @@ int32_t LinuxInput::runOnce()
|
||||
if (firstTime) {
|
||||
if (portduino_config.keyboardDevice == "")
|
||||
return disable();
|
||||
fd = open(portduino_config.keyboardDevice.c_str(), O_RDWR);
|
||||
fd = open(portduino_config.keyboardDevice.c_str(), O_RDWR | O_CLOEXEC);
|
||||
if (fd < 0)
|
||||
return disable();
|
||||
ret = ioctl(fd, EVIOCGRAB, (void *)1);
|
||||
if (ret != 0)
|
||||
return disable();
|
||||
|
||||
epollfd = epoll_create1(0);
|
||||
epollfd = epoll_create1(EPOLL_CLOEXEC);
|
||||
assert(epollfd >= 0);
|
||||
|
||||
ev.events = EPOLLIN;
|
||||
|
||||
@@ -44,7 +44,7 @@ class LinuxInput : public Observable<const InputEvent *>, public concurrency::OS
|
||||
int fd = -1;
|
||||
int ret;
|
||||
uint8_t report[8];
|
||||
int epollfd;
|
||||
int epollfd = -1;
|
||||
struct epoll_event ev;
|
||||
uint8_t modifiers = 0;
|
||||
std::map<int, char> keymap{
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
#include "configuration.h"
|
||||
#if ARCH_PORTDUINO && defined(__linux__)
|
||||
#include "InputBroker.h"
|
||||
#include "LinuxJoystick.h"
|
||||
#include "platform/portduino/PortduinoGlue.h"
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
LinuxJoystick *aLinuxJoystick;
|
||||
|
||||
LinuxJoystick::LinuxJoystick(const char *name) : concurrency::OSThread(name)
|
||||
{
|
||||
this->_originName = name;
|
||||
}
|
||||
|
||||
// Translate a config.yaml action name into a broker event. Returns INPUT_BROKER_NONE
|
||||
// for unknown names so a typo simply leaves that button unmapped.
|
||||
static input_broker_event joystickActionToEvent(const std::string &action)
|
||||
{
|
||||
if (action == "select")
|
||||
return INPUT_BROKER_SELECT;
|
||||
if (action == "cancel")
|
||||
return INPUT_BROKER_CANCEL;
|
||||
if (action == "back")
|
||||
return INPUT_BROKER_BACK;
|
||||
if (action == "up")
|
||||
return INPUT_BROKER_UP;
|
||||
if (action == "down")
|
||||
return INPUT_BROKER_DOWN;
|
||||
if (action == "left")
|
||||
return INPUT_BROKER_LEFT;
|
||||
if (action == "right")
|
||||
return INPUT_BROKER_RIGHT;
|
||||
if (action == "user" || action == "userpress")
|
||||
return INPUT_BROKER_USER_PRESS;
|
||||
return INPUT_BROKER_NONE;
|
||||
}
|
||||
|
||||
void LinuxJoystick::init()
|
||||
{
|
||||
if (portduino_config.joystickButtons.empty()) {
|
||||
// No config override: fall back to the built-in defaults.
|
||||
buttonMap[JOY_BTN_A] = INPUT_BROKER_SELECT;
|
||||
buttonMap[JOY_BTN_B] = INPUT_BROKER_CANCEL;
|
||||
} else {
|
||||
for (const auto &button : portduino_config.joystickButtons) {
|
||||
input_broker_event event = joystickActionToEvent(button.second);
|
||||
if (event != INPUT_BROKER_NONE)
|
||||
buttonMap[button.first] = event;
|
||||
}
|
||||
}
|
||||
inputBroker->registerSource(this);
|
||||
}
|
||||
|
||||
void LinuxJoystick::deInit()
|
||||
{
|
||||
// reboot() on native does execv() in-place, which only closes O_CLOEXEC fds.
|
||||
// Close both descriptors here so we don't leak one per restart.
|
||||
if (fd >= 0)
|
||||
close(fd);
|
||||
if (epollfd >= 0)
|
||||
close(epollfd);
|
||||
fd = -1;
|
||||
epollfd = -1;
|
||||
}
|
||||
|
||||
// Bucket a raw axis value into a direction zone: -1 (min edge), 0 (center), +1 (max edge).
|
||||
static int axisZone(int value)
|
||||
{
|
||||
if (value <= JOY_AXIS_LOW)
|
||||
return -1;
|
||||
if (value >= JOY_AXIS_HIGH)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void LinuxJoystick::emitEvent(input_broker_event event)
|
||||
{
|
||||
InputEvent e = {};
|
||||
e.inputEvent = event;
|
||||
e.source = this->_originName;
|
||||
e.kbchar = 0;
|
||||
// LOG_DEBUG("joystick: %s event %d", this->_originName, event);
|
||||
this->notifyObservers(&e);
|
||||
}
|
||||
|
||||
int32_t LinuxJoystick::runOnce()
|
||||
{
|
||||
if (firstTime) {
|
||||
if (portduino_config.joystickDevice == "")
|
||||
return disable();
|
||||
fd = open(portduino_config.joystickDevice.c_str(), O_RDWR | O_CLOEXEC);
|
||||
if (fd < 0)
|
||||
return disable();
|
||||
if (ioctl(fd, EVIOCGRAB, (void *)1) != 0)
|
||||
return disable();
|
||||
|
||||
epollfd = epoll_create1(EPOLL_CLOEXEC);
|
||||
assert(epollfd >= 0);
|
||||
|
||||
ev.events = EPOLLIN;
|
||||
ev.data.fd = fd;
|
||||
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev)) {
|
||||
perror("joystick: unable to epoll add");
|
||||
return disable();
|
||||
}
|
||||
// This is the first time the OSThread library has called this function, so do port setup
|
||||
firstTime = false;
|
||||
}
|
||||
|
||||
int nfds = epoll_wait(epollfd, events, JOY_MAX_EVENTS, 1);
|
||||
if (nfds < 0) {
|
||||
perror("joystick: epoll_wait failed");
|
||||
return disable();
|
||||
}
|
||||
|
||||
for (int i = 0; i < nfds; i++) {
|
||||
struct input_event evs[64];
|
||||
int rd = read(events[i].data.fd, evs, sizeof(evs));
|
||||
if (rd < (signed int)sizeof(struct input_event))
|
||||
continue;
|
||||
for (int j = 0; j < rd / ((signed int)sizeof(struct input_event)); j++) {
|
||||
unsigned int type = evs[j].type;
|
||||
unsigned int code = evs[j].code;
|
||||
int value = evs[j].value;
|
||||
|
||||
if (type == EV_ABS) {
|
||||
// D-pad reports as ABS_X / ABS_Y with digital 0 / 127 / 255 values. Emit on the
|
||||
// transition to an edge and arm auto-repeat; a held direction repeats below.
|
||||
if (code == ABS_X) {
|
||||
int zone = axisZone(value);
|
||||
if (zone != heldX) {
|
||||
heldX = zone;
|
||||
if (zone != 0) {
|
||||
emitEvent((zone < 0) ? INPUT_BROKER_LEFT : INPUT_BROKER_RIGHT);
|
||||
nextRepeatX = millis() + JOY_REPEAT_DELAY_MS;
|
||||
}
|
||||
}
|
||||
} else if (code == ABS_Y) {
|
||||
int zone = axisZone(value);
|
||||
if (zone != heldY) {
|
||||
heldY = zone;
|
||||
if (zone != 0) {
|
||||
emitEvent((zone < 0) ? INPUT_BROKER_UP : INPUT_BROKER_DOWN);
|
||||
nextRepeatY = millis() + JOY_REPEAT_DELAY_MS;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (type == EV_KEY && value == 1) {
|
||||
// Look up the configured action for this button; unmapped buttons are ignored.
|
||||
// Buttons fire once per press (no auto-repeat).
|
||||
auto mapped = buttonMap.find(code);
|
||||
if (mapped != buttonMap.end())
|
||||
emitEvent(mapped->second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-repeat held D-pad directions. Signed comparison is wraparound-safe.
|
||||
uint32_t now = millis();
|
||||
if (heldX != 0 && (int32_t)(now - nextRepeatX) >= 0) {
|
||||
emitEvent((heldX < 0) ? INPUT_BROKER_LEFT : INPUT_BROKER_RIGHT);
|
||||
nextRepeatX = now + JOY_REPEAT_INTERVAL_MS;
|
||||
}
|
||||
if (heldY != 0 && (int32_t)(now - nextRepeatY) >= 0) {
|
||||
emitEvent((heldY < 0) ? INPUT_BROKER_UP : INPUT_BROKER_DOWN);
|
||||
nextRepeatY = now + JOY_REPEAT_INTERVAL_MS;
|
||||
}
|
||||
|
||||
return 50; // Poll every 50msec
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,65 @@
|
||||
#pragma once
|
||||
// Linux evdev gamepad/joystick input. Only compiled on Linux portduino targets;
|
||||
// macOS / non-Linux builds have no <linux/input.h> or epoll. Unlike LinuxInput
|
||||
// (keyboard, EV_KEY only) this decodes the D-pad from EV_ABS axes as well.
|
||||
#if ARCH_PORTDUINO && defined(__linux__)
|
||||
#include "InputBroker.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include <linux/input.h>
|
||||
#include <map>
|
||||
#include <stdint.h>
|
||||
#include <sys/epoll.h>
|
||||
|
||||
#define JOY_MAX_EVENTS 10
|
||||
|
||||
// Default button map when config.yaml has no [Input] JoystickButtons override.
|
||||
// Codes confirmed by evdev capture on a 0079:0011 "USB Gamepad".
|
||||
#define JOY_BTN_A BTN_THUMB // 0x121 -> INPUT_BROKER_SELECT
|
||||
#define JOY_BTN_B BTN_THUMB2 // 0x122 -> INPUT_BROKER_CANCEL
|
||||
#define JOY_AXIS_CENTER 127 // D-pad resting value
|
||||
#define JOY_AXIS_LOW 64 // below this -> "min" edge (0)
|
||||
#define JOY_AXIS_HIGH 192 // above this -> "max" edge (255)
|
||||
|
||||
// D-pad auto-repeat while a direction is held (typematic).
|
||||
#define JOY_REPEAT_DELAY_MS 400 // hold this long before repeats start
|
||||
#define JOY_REPEAT_INTERVAL_MS 150 // then repeat this often
|
||||
|
||||
class LinuxJoystick : public Observable<const InputEvent *>, public concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
explicit LinuxJoystick(const char *name);
|
||||
void init(); // Registers this source with the InputBroker
|
||||
void deInit(); // Strictly for cleanly "rebooting" the binary on native
|
||||
|
||||
// Current held D-pad zone, updated the instant the axis moves (independent of the typematic
|
||||
// auto-repeat). -1 = left/up edge, 0 = centered, +1 = right/down edge. Lets a game poll for
|
||||
// smooth continuous control instead of waiting for the slow repeat events.
|
||||
int heldXZone() const { return heldX; }
|
||||
int heldYZone() const { return heldY; }
|
||||
|
||||
protected:
|
||||
virtual int32_t runOnce() override;
|
||||
|
||||
private:
|
||||
void emitEvent(input_broker_event event);
|
||||
|
||||
const char *_originName;
|
||||
bool firstTime = true;
|
||||
|
||||
// evdev button code -> broker event, built from config (or defaults) in init().
|
||||
std::map<int, input_broker_event> buttonMap;
|
||||
|
||||
struct epoll_event events[JOY_MAX_EVENTS];
|
||||
struct epoll_event ev;
|
||||
int fd = -1;
|
||||
int epollfd = -1;
|
||||
|
||||
// D-pad auto-repeat state: currently held zone per axis (-1 / 0 / +1) and the
|
||||
// next millis() timestamp at which to re-emit while the direction is held.
|
||||
int heldX = 0;
|
||||
int heldY = 0;
|
||||
uint32_t nextRepeatX = 0;
|
||||
uint32_t nextRepeatY = 0;
|
||||
};
|
||||
extern LinuxJoystick *aLinuxJoystick;
|
||||
#endif
|
||||
+17
-1
@@ -22,13 +22,13 @@
|
||||
|
||||
#include "FSCommon.h"
|
||||
#include "Power.h"
|
||||
#include "RTC.h"
|
||||
#include "SPILock.h"
|
||||
#include "Throttle.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "concurrency/Periodic.h"
|
||||
#include "detect/ScanI2C.h"
|
||||
#include "error.h"
|
||||
#include "gps/RTC.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_I2C
|
||||
#include "detect/ScanI2CConsumer.h"
|
||||
@@ -196,8 +196,16 @@ void setupNicheGraphics();
|
||||
#endif
|
||||
|
||||
#if defined(HW_SPI1_DEVICE) && defined(ARCH_ESP32)
|
||||
#if defined(HAS_SDCARD) && defined(SDCARD_USE_SPI1)
|
||||
// Reuse FSCommon's SPI_HSPI instance to avoid double-initializing SPI2_HOST in arduino-esp32 3.x.
|
||||
// Two SPIClass(HSPI) objects on the same bus cause the second spi_bus_initialize() to return
|
||||
// ESP_ERR_INVALID_STATE, leaving the LoRa device handle invalid and blocking SPI transfers.
|
||||
extern SPIClass SPI_HSPI;
|
||||
SPIClass &SPI1 = SPI_HSPI;
|
||||
#else
|
||||
SPIClass SPI1(HSPI);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
using namespace concurrency;
|
||||
|
||||
@@ -420,10 +428,14 @@ void setup()
|
||||
#if ARCH_PORTDUINO
|
||||
RTCQuality ourQuality = RTCQualityDevice;
|
||||
|
||||
#ifdef __linux__
|
||||
// timedatectl is systemd-only, so macOS, Windows and WASM stay at
|
||||
// RTCQualityDevice rather than claim NTP quality we have not verified.
|
||||
std::string timeCommandResult = exec("timedatectl status | grep synchronized | grep yes -c");
|
||||
if (timeCommandResult[0] == '1') {
|
||||
ourQuality = RTCQualityNTP;
|
||||
}
|
||||
#endif
|
||||
|
||||
struct timeval tv;
|
||||
tv.tv_sec = time(NULL);
|
||||
@@ -808,6 +820,10 @@ void setup()
|
||||
rp2040Setup();
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_STM32WL
|
||||
stm32wlSetup();
|
||||
#endif
|
||||
|
||||
// We do this as early as possible because this loads preferences from flash
|
||||
// but we need to do this after main cpu init (esp32setup), because we need the random seed set
|
||||
nodeDB = new NodeDB;
|
||||
|
||||
+2
-1
@@ -113,7 +113,8 @@ extern bool runASAP;
|
||||
|
||||
extern bool pauseBluetoothLogging;
|
||||
|
||||
void nrf52Setup(), esp32Setup(), nrf52Loop(), esp32Loop(), rp2040Setup(), rp2040Loop(), clearBonds(), enterDfuMode();
|
||||
void nrf52Setup(), esp32Setup(), nrf52Loop(), esp32Loop(), rp2040Setup(), rp2040Loop(), clearBonds(), enterDfuMode(),
|
||||
stm32wlSetup();
|
||||
#ifdef ARCH_ESP32
|
||||
void esp32ReleaseBluetoothMemoryIfUnused();
|
||||
#endif
|
||||
|
||||
@@ -516,7 +516,7 @@ bool Channels::hasDefaultChannel()
|
||||
*/
|
||||
bool Channels::decryptForHash(ChannelIndex chIndex, ChannelHash channelHash)
|
||||
{
|
||||
if (chIndex > getNumChannels() || getHash(chIndex) != channelHash) {
|
||||
if (chIndex >= getNumChannels() || getHash(chIndex) != channelHash) {
|
||||
// LOG_DEBUG("Skip channel %d (hash %x) due to invalid hash/index, want=%x", chIndex, getHash(chIndex),
|
||||
// channelHash);
|
||||
return false;
|
||||
|
||||
@@ -224,7 +224,11 @@ bool CryptoEngine::encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtas
|
||||
uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut)
|
||||
{
|
||||
uint8_t *auth;
|
||||
long extraNonceTmp = random();
|
||||
// The extra nonce must be unpredictable: use the hardware RNG, falling back to the
|
||||
// seeded CSPRNG only when no hardware source is available.
|
||||
uint32_t extraNonceTmp;
|
||||
if (!HardwareRNG::fill((uint8_t *)&extraNonceTmp, sizeof(extraNonceTmp)))
|
||||
CryptRNG.rand((uint8_t *)&extraNonceTmp, sizeof(extraNonceTmp));
|
||||
auth = bytesOut + numBytes;
|
||||
memcpy((uint8_t *)(auth + 8), &extraNonceTmp,
|
||||
sizeof(uint32_t)); // do not use dereference on potential non aligned pointers : *extraNonce = extraNonceTmp;
|
||||
@@ -341,6 +345,32 @@ bool CryptoEngine::setDHPublicKey(uint8_t *pubKey)
|
||||
return true;
|
||||
}
|
||||
|
||||
void CryptoEngine::setPendingPublicKey(uint32_t node, const uint8_t *key)
|
||||
{
|
||||
concurrency::LockGuard g(&pendingKeyLock);
|
||||
pendingKeyVerificationNode = node;
|
||||
memcpy(pendingKeyVerificationPublicKey, key, 32);
|
||||
hasPendingKeyVerificationKey = true;
|
||||
}
|
||||
|
||||
void CryptoEngine::clearPendingPublicKey()
|
||||
{
|
||||
concurrency::LockGuard g(&pendingKeyLock);
|
||||
pendingKeyVerificationNode = 0;
|
||||
memset(pendingKeyVerificationPublicKey, 0, 32);
|
||||
hasPendingKeyVerificationKey = false;
|
||||
}
|
||||
|
||||
bool CryptoEngine::getPendingPublicKey(uint32_t node, meshtastic_NodeInfoLite_public_key_t &out)
|
||||
{
|
||||
concurrency::LockGuard g(&pendingKeyLock);
|
||||
if (!hasPendingKeyVerificationKey || node == 0 || node != pendingKeyVerificationNode)
|
||||
return false;
|
||||
out.size = 32;
|
||||
memcpy(out.bytes, pendingKeyVerificationPublicKey, 32);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
concurrency::Lock *cryptLock;
|
||||
|
||||
|
||||
@@ -59,6 +59,17 @@ class CryptoEngine
|
||||
virtual bool setDHPublicKey(uint8_t *publicKey);
|
||||
virtual void hash(uint8_t *bytes, size_t numBytes);
|
||||
|
||||
// Temporary holder for a peer's not-yet-verified public key, learned in-band during an
|
||||
// in-progress key-verification handshake before it is committed to NodeDB. Lets the Router
|
||||
// run the DH handshake to encode/decode the follow-on PKI packet. Single slot is enough:
|
||||
// only one verification runs at a time. Discarded when the handshake ends (resetToIdle).
|
||||
// Internally guarded by pendingKeyLock, not cryptLock: the Router calls the getter while
|
||||
// already holding the non-recursive cryptLock; KeyVerificationModule writes from elsewhere.
|
||||
void setPendingPublicKey(uint32_t node, const uint8_t *key);
|
||||
void clearPendingPublicKey();
|
||||
// Fills `out` (size set to 32) and returns true iff a pending key is held for `node`.
|
||||
bool getPendingPublicKey(uint32_t node, meshtastic_NodeInfoLite_public_key_t &out);
|
||||
|
||||
virtual void aesSetKey(const uint8_t *key, size_t key_len);
|
||||
|
||||
virtual void aesEncrypt(uint8_t *in, uint8_t *out);
|
||||
@@ -94,6 +105,10 @@ class CryptoEngine
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||
uint8_t shared_key[32] = {0};
|
||||
uint8_t private_key[32] = {0};
|
||||
uint32_t pendingKeyVerificationNode = 0;
|
||||
uint8_t pendingKeyVerificationPublicKey[32] = {0};
|
||||
bool hasPendingKeyVerificationKey = false;
|
||||
concurrency::Lock pendingKeyLock;
|
||||
#if !(MESHTASTIC_EXCLUDE_XEDDSA)
|
||||
uint8_t xeddsa_public_key[32] = {0};
|
||||
uint8_t xeddsa_private_key[32] = {0};
|
||||
|
||||
@@ -22,6 +22,12 @@ extern Adafruit_nRFCrypto nRFCrypto;
|
||||
#include <unistd.h>
|
||||
#ifdef __linux__
|
||||
#include <sys/random.h> // getrandom()
|
||||
#elif defined(_WIN32)
|
||||
// Order is load-bearing, hence the blank line: bcrypt.h uses LONG/ULONG from
|
||||
// windows.h and does not include it itself.
|
||||
#include <windows.h>
|
||||
|
||||
#include <bcrypt.h> // BCryptGenRandom()
|
||||
#else
|
||||
#include <stdlib.h> // arc4random_buf() on Darwin/BSD
|
||||
#endif
|
||||
@@ -128,6 +134,12 @@ bool fill(uint8_t *buffer, size_t length, bool useRadioEntropy)
|
||||
if (generated == static_cast<ssize_t>(length)) {
|
||||
filled = true;
|
||||
}
|
||||
#elif defined(_WIN32)
|
||||
// No getrandom/arc4random on Windows; BCryptGenRandom is the documented CSPRNG.
|
||||
// Preferred over std::random_device, whose libstdc++ Windows backend reports entropy() == 0.
|
||||
if (BCryptGenRandom(NULL, buffer, static_cast<ULONG>(length), BCRYPT_USE_SYSTEM_PREFERRED_RNG) == 0) { // STATUS_SUCCESS
|
||||
filled = true;
|
||||
}
|
||||
#elif defined(__EMSCRIPTEN__)
|
||||
// Browser/wasm: no getrandom/arc4random - fall through to std::random_device,
|
||||
// which emscripten backs with crypto.getRandomValues().
|
||||
|
||||
@@ -229,6 +229,11 @@ static inline void modemPresetToParams(meshtastic_Config_LoRaConfig_ModemPreset
|
||||
cr = 5;
|
||||
sf = 10;
|
||||
break;
|
||||
case PRESET(MEDIUM_TURBO):
|
||||
bwKHz = wideLora ? 1625.0f : 500.0f;
|
||||
cr = 5;
|
||||
sf = 9;
|
||||
break;
|
||||
case PRESET(LONG_TURBO):
|
||||
bwKHz = wideLora ? 1625.0f : 500.0f;
|
||||
cr = 8;
|
||||
|
||||
@@ -11,12 +11,13 @@
|
||||
#include "NodeDB.h"
|
||||
#include "Power.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "TypeConversions.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "graphics/draw/MessageRenderer.h"
|
||||
#include "main.h"
|
||||
#include "mesh-pb-constants.h"
|
||||
#include "meshUtils.h"
|
||||
#include "modules/AdminModule.h"
|
||||
#include "modules/NodeInfoModule.h"
|
||||
#include "modules/PositionModule.h"
|
||||
#include "modules/RoutingModule.h"
|
||||
@@ -63,6 +64,7 @@ Allocator<meshtastic_ClientNotification> &clientNotificationPool = staticClientN
|
||||
|
||||
Allocator<meshtastic_QueueStatus> &queueStatusPool = staticQueueStatusPool;
|
||||
|
||||
#include "PositionPrecision.h"
|
||||
#include "Router.h"
|
||||
|
||||
MeshService::MeshService()
|
||||
@@ -173,6 +175,51 @@ NodeNum MeshService::getNodenumFromRequestId(uint32_t request_id)
|
||||
return nodenum;
|
||||
}
|
||||
|
||||
#if MESHTASTIC_ENABLE_FRAME_INJECTION
|
||||
// Deliver a client-supplied frame into the receive pipeline as if it arrived off the LoRa chip. Mirrors
|
||||
// the portduino SimRadio SIMULATOR_APP unwrap so the same host wire format works on real hardware: the
|
||||
// frame rides inside a Compressed envelope wrapped in a MeshPacket that carries from/to/id/channel.
|
||||
// Compressed.portnum == UNKNOWN_APP -> Compressed.data is verbatim ciphertext, decrypted as if off-air
|
||||
// otherwise -> Compressed.data is the plaintext payload for Compressed.portnum
|
||||
void MeshService::injectAsReceived(meshtastic_MeshPacket &p)
|
||||
{
|
||||
meshtastic_Compressed scratch;
|
||||
if (p.which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
|
||||
memset(&scratch, 0, sizeof(scratch));
|
||||
if (pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_Compressed_msg, &scratch)) {
|
||||
if (scratch.portnum == meshtastic_PortNum_UNKNOWN_APP) {
|
||||
p.which_payload_variant = meshtastic_MeshPacket_encrypted_tag;
|
||||
memcpy(p.encrypted.bytes, scratch.data.bytes, scratch.data.size);
|
||||
p.encrypted.size = scratch.data.size;
|
||||
} else {
|
||||
memcpy(&p.decoded.payload, &scratch.data, sizeof(scratch.data));
|
||||
p.decoded.portnum = scratch.portnum;
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR("inject: could not decode Compressed envelope, dropping");
|
||||
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)");
|
||||
return;
|
||||
}
|
||||
meshtastic_MeshPacket *mp = packetPool.allocCopy(p);
|
||||
if (!mp)
|
||||
return;
|
||||
if (mp->rx_snr == 0) // plausible synthetic link metadata unless the caller set it
|
||||
mp->rx_snr = 8;
|
||||
if (mp->rx_rssi == 0)
|
||||
mp->rx_rssi = -40;
|
||||
mp->rx_time = getValidTime(RTCQualityFromNet);
|
||||
LOG_INFO("inject: RX from=0x%08x to=0x%08x id=0x%08x ch=%d %s", mp->from, mp->to, mp->id, mp->channel,
|
||||
mp->which_payload_variant == meshtastic_MeshPacket_encrypted_tag ? "encrypted" : "decoded");
|
||||
router->enqueueReceivedMessage(mp);
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Given a ToRadio buffer parse it and properly handle it (setup radio, owner or send packet into the mesh)
|
||||
* Called by PhoneAPI.handleToRadio. Note: p is a scratch buffer, this function is allowed to write to it but it can not keep a
|
||||
@@ -186,6 +233,16 @@ void MeshService::handleToRadio(meshtastic_MeshPacket &p)
|
||||
SimRadio::instance->unpackAndReceive(p);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#if MESHTASTIC_ENABLE_FRAME_INJECTION
|
||||
// Real-hardware analog of the SimRadio path above: deliver a client-supplied frame into the RX
|
||||
// pipeline exactly as if it had arrived off the LoRa chip. Reached before the p.from=0 line below,
|
||||
// so an injected sender is preserved. Build-flag gated (off by default) - it lets anything with a
|
||||
// wired connection forge over-the-air traffic, so it must never ship enabled.
|
||||
if (p.which_payload_variant == meshtastic_MeshPacket_decoded_tag && p.decoded.portnum == meshtastic_PortNum_SIMULATOR_APP) {
|
||||
injectAsReceived(p);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
p.from = 0; // We don't let clients assign nodenums to their sent messages
|
||||
p.next_hop = NO_NEXT_HOP_PREFERENCE; // We don't let clients assign next_hop to their sent messages
|
||||
@@ -200,9 +257,17 @@ void MeshService::handleToRadio(meshtastic_MeshPacket &p)
|
||||
p.to != NODENUM_BROADCAST && p.to != 0) // DM only
|
||||
{
|
||||
perhapsDecode(&p);
|
||||
const StoredMessage &sm = messageStore.addFromPacket(p);
|
||||
graphics::MessageRenderer::handleNewMessage(nullptr, sm, p); // notify UI
|
||||
if (const StoredMessage *sm = messageStore.tryAddFromPacket(p))
|
||||
graphics::MessageRenderer::handleNewMessage(nullptr, *sm, p); // notify UI
|
||||
})
|
||||
#if !MESHTASTIC_EXCLUDE_ADMIN
|
||||
// Note admin requests on their way out: AdminModule only accepts a response from a remote we
|
||||
// actually asked. Runs before encryption, while the payload is still readable.
|
||||
if (adminModule && p.which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
|
||||
p.decoded.portnum == meshtastic_PortNum_ADMIN_APP)
|
||||
adminModule->noteOutgoingAdminRequest(p);
|
||||
#endif
|
||||
|
||||
// Send the packet into the mesh
|
||||
DEBUG_HEAP_BEFORE;
|
||||
auto a = packetPool.allocCopy(p);
|
||||
@@ -293,8 +358,31 @@ bool MeshService::trySendPosition(NodeNum dest, bool wantReplies)
|
||||
LOG_DEBUG("Skip position ping; no fresh position since boot");
|
||||
return false;
|
||||
}
|
||||
LOG_INFO("Send position ping to 0x%08x, wantReplies=%d, channel=%d", dest, wantReplies, node->channel);
|
||||
positionModule->sendOurPosition(dest, wantReplies, node->channel);
|
||||
// Prefer the node's current channel, but fall back to the first channel with
|
||||
// position enabled (matching PositionModule::sendOurPosition() behavior).
|
||||
uint8_t sendChan = node->channel;
|
||||
if (getPositionPrecisionForChannel(sendChan) == 0) {
|
||||
bool found = false;
|
||||
for (uint8_t ch = 0; ch < 8; ++ch) {
|
||||
if (getPositionPrecisionForChannel(ch) != 0) {
|
||||
sendChan = ch;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
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);
|
||||
nodeInfoModule->sendOurNodeInfo(dest, wantReplies, node->channel);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
LOG_INFO("Send position ping to 0x%08x, wantReplies=%d, channel=%d", dest, wantReplies, sendChan);
|
||||
positionModule->sendOurPosition(dest, wantReplies, sendChan);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -159,6 +159,12 @@ class MeshService
|
||||
*/
|
||||
void handleToRadio(meshtastic_MeshPacket &p);
|
||||
|
||||
#if MESHTASTIC_ENABLE_FRAME_INJECTION
|
||||
/// Test/debug seam (build-flag gated, off by default): deliver a client-supplied frame into the
|
||||
/// receive pipeline as if it had arrived off the LoRa chip. See the definition for the wire format.
|
||||
void injectAsReceived(meshtastic_MeshPacket &p);
|
||||
#endif
|
||||
|
||||
/** The radioConfig object just changed, call this to force the hw to change to the new settings
|
||||
* @return true if client devices should be sent a new set of radio configs
|
||||
*/
|
||||
|
||||
+62
-71
@@ -13,7 +13,6 @@
|
||||
#include "NodeDB.h"
|
||||
#include "PacketHistory.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "RadioInterface.h"
|
||||
#include "Router.h"
|
||||
#include "SPILock.h"
|
||||
@@ -21,12 +20,14 @@
|
||||
#include "TransmitHistory.h"
|
||||
#include "TypeConversions.h"
|
||||
#include "error.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "main.h"
|
||||
#include "memory/MemAudit.h"
|
||||
#include "mesh-pb-constants.h"
|
||||
#include "mesh/generated/meshtastic/deviceonly_legacy.pb.h"
|
||||
#include "meshUtils.h"
|
||||
#include "modules/NeighborInfoModule.h"
|
||||
#include "target_specific.h"
|
||||
#if HAS_VARIABLE_HOPS
|
||||
#include "modules/HopScalingModule.h"
|
||||
#endif
|
||||
@@ -50,11 +51,7 @@
|
||||
#include "SPILock.h"
|
||||
#include "modules/StoreForwardModule.h"
|
||||
#include <Preferences.h>
|
||||
#include <esp_efuse.h>
|
||||
#include <esp_efuse_table.h>
|
||||
#include <nvs_flash.h>
|
||||
#include <soc/efuse_reg.h>
|
||||
#include <soc/soc.h>
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_PORTDUINO
|
||||
@@ -372,8 +369,7 @@ void NodeDB::disarmNodeDatabaseDecodeTargets()
|
||||
*/
|
||||
uint32_t radioGeneration;
|
||||
|
||||
// FIXME - move this somewhere else
|
||||
extern void getMacAddr(uint8_t *dmac);
|
||||
// getMacAddr() and getDeviceId() are the per-architecture hooks declared in target_specific.h.
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -410,53 +406,13 @@ NodeDB::NodeDB()
|
||||
uint32_t channelFileCRC = crc32Buffer(&channelFile, sizeof(channelFile));
|
||||
|
||||
int saveWhat = 0;
|
||||
// Get device unique id
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6)
|
||||
uint32_t unique_id[4];
|
||||
// ESP32 factory burns a unique id in efuse for S2+ series and evidently C3+ series
|
||||
// This is used for HMACs in the esp-rainmaker AIOT platform and seems to be a good choice for us
|
||||
esp_err_t err = esp_efuse_read_field_blob(ESP_EFUSE_OPTIONAL_UNIQUE_ID, unique_id, sizeof(unique_id) * 8);
|
||||
if (err == ESP_OK) {
|
||||
memcpy(myNodeInfo.device_id.bytes, unique_id, sizeof(unique_id));
|
||||
myNodeInfo.device_id.size = 16;
|
||||
} else {
|
||||
LOG_WARN("Failed to read unique id from efuse");
|
||||
// Re-read the device id from silicon each boot via the per-arch getDeviceId(); clear the
|
||||
// disk-loaded value first so a failed/empty derivation leaves it unset rather than stale.
|
||||
myNodeInfo.device_id.size = 0;
|
||||
memset(myNodeInfo.device_id.bytes, 0, sizeof(myNodeInfo.device_id.bytes));
|
||||
if (getDeviceId(myNodeInfo.device_id.bytes)) {
|
||||
myNodeInfo.device_id.size = sizeof(myNodeInfo.device_id.bytes);
|
||||
}
|
||||
#elif defined(ARCH_NRF54L15)
|
||||
// nRF54L15: DEVICEID is under FICR->INFO sub-struct (not top-level as on nRF52)
|
||||
uint64_t device_id_start = ((uint64_t)NRF_FICR->INFO.DEVICEID[1] << 32) | NRF_FICR->INFO.DEVICEID[0];
|
||||
uint64_t device_id_end = ((uint64_t)NRF_FICR->DEVICEADDR[1] << 32) | NRF_FICR->DEVICEADDR[0];
|
||||
memcpy(myNodeInfo.device_id.bytes, &device_id_start, sizeof(device_id_start));
|
||||
memcpy(myNodeInfo.device_id.bytes + sizeof(device_id_start), &device_id_end, sizeof(device_id_end));
|
||||
myNodeInfo.device_id.size = 16;
|
||||
#elif defined(ARCH_NRF52)
|
||||
// Nordic applies a FIPS compliant Random ID to each chip at the factory
|
||||
// We concatenate the device address to the Random ID to create a unique ID for now
|
||||
// This will likely utilize a crypto module in the future
|
||||
uint64_t device_id_start = ((uint64_t)NRF_FICR->DEVICEID[1] << 32) | NRF_FICR->DEVICEID[0];
|
||||
uint64_t device_id_end = ((uint64_t)NRF_FICR->DEVICEADDR[1] << 32) | NRF_FICR->DEVICEADDR[0];
|
||||
memcpy(myNodeInfo.device_id.bytes, &device_id_start, sizeof(device_id_start));
|
||||
memcpy(myNodeInfo.device_id.bytes + sizeof(device_id_start), &device_id_end, sizeof(device_id_end));
|
||||
myNodeInfo.device_id.size = 16;
|
||||
// Uncomment below to print the device id
|
||||
#elif ARCH_PORTDUINO
|
||||
if (portduino_config.has_device_id) {
|
||||
memcpy(myNodeInfo.device_id.bytes, portduino_config.device_id, 16);
|
||||
myNodeInfo.device_id.size = 16;
|
||||
}
|
||||
#else
|
||||
// FIXME - implement for other platforms
|
||||
#endif
|
||||
|
||||
// if (myNodeInfo.device_id.size == 16) {
|
||||
// std::string deviceIdHex;
|
||||
// for (size_t i = 0; i < myNodeInfo.device_id.size; ++i) {
|
||||
// char buf[3];
|
||||
// snprintf(buf, sizeof(buf), "%02X", myNodeInfo.device_id.bytes[i]);
|
||||
// deviceIdHex += buf;
|
||||
// }
|
||||
// LOG_DEBUG("Device ID (HEX): %s", deviceIdHex.c_str());
|
||||
// }
|
||||
|
||||
// likewise - we always want the app requirements to come from the running appload
|
||||
myNodeInfo.min_app_version = 30200; // format is Mmmss (where M is 1+the numeric major number. i.e. 30200 means 2.2.00
|
||||
@@ -522,6 +478,12 @@ NodeDB::NodeDB()
|
||||
LOG_DEBUG("Number of 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");
|
||||
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
||||
}
|
||||
|
||||
resetRadioConfig(); // If bogus settings got saved, then fix them
|
||||
// nodeDB->LOG_DEBUG("region=%d, NODENUM=0x%x, dbsize=%d", config.lora.region, myNodeInfo.my_node_num, numMeshNodes);
|
||||
|
||||
@@ -1564,16 +1526,34 @@ void NodeDB::initModuleConfigIntervals()
|
||||
moduleConfig.telemetry.device_update_interval = MAX_INTERVAL;
|
||||
#endif
|
||||
|
||||
#ifdef USERPREFS_CONFIG_ENVIRONMENT_MEASUREMENT_ENABLED
|
||||
moduleConfig.telemetry.environment_measurement_enabled = USERPREFS_CONFIG_ENVIRONMENT_MEASUREMENT_ENABLED;
|
||||
#endif
|
||||
|
||||
#ifdef USERPREFS_CONFIG_ENV_TELEM_UPDATE_INTERVAL
|
||||
moduleConfig.telemetry.environment_update_interval = USERPREFS_CONFIG_ENV_TELEM_UPDATE_INTERVAL;
|
||||
#else
|
||||
moduleConfig.telemetry.environment_update_interval = 0;
|
||||
#endif
|
||||
|
||||
#ifdef USERPREFS_CONFIG_ENVIRONMENT_MEASUREMENT_ENABLED
|
||||
moduleConfig.telemetry.environment_measurement_enabled = USERPREFS_CONFIG_ENVIRONMENT_MEASUREMENT_ENABLED;
|
||||
#ifdef USERPREFS_CONFIG_ENV_SCREEN_SCREEN_ENABLED
|
||||
moduleConfig.telemetry.environment_screen_enabled = USERPREFS_CONFIG_ENV_SCREEN_SCREEN_ENABLED;
|
||||
#endif
|
||||
|
||||
#ifdef USERPREFS_CONFIG_AQ_TELEM_UPDATE_INTERVAL
|
||||
moduleConfig.telemetry.air_quality_interval = USERPREFS_CONFIG_AQ_TELEM_UPDATE_INTERVAL;
|
||||
#else
|
||||
moduleConfig.telemetry.air_quality_interval = 0;
|
||||
#endif
|
||||
|
||||
#ifdef USERPREFS_CONFIG_AQ_MEASUREMENT_ENABLED
|
||||
moduleConfig.telemetry.air_quality_enabled = USERPREFS_CONFIG_AQ_MEASUREMENT_ENABLED;
|
||||
#endif
|
||||
|
||||
#ifdef USERPREFS_CONFIG_AQ_SCREEN_ENABLED
|
||||
moduleConfig.telemetry.air_quality_screen_enabled = USERPREFS_CONFIG_AQ_SCREEN_ENABLED;
|
||||
#endif
|
||||
|
||||
moduleConfig.telemetry.power_update_interval = 0;
|
||||
moduleConfig.telemetry.health_update_interval = 0;
|
||||
moduleConfig.neighbor_info.update_interval = 0;
|
||||
@@ -1635,10 +1615,16 @@ void NodeDB::removeNodeByNum(NodeNum nodeNum)
|
||||
removed++;
|
||||
}
|
||||
numMeshNodes -= removed;
|
||||
std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.begin() + numMeshNodes + 1,
|
||||
meshtastic_NodeInfoLite());
|
||||
if (removed)
|
||||
eraseNodeSatellites(nodeNum);
|
||||
if (removed) {
|
||||
// Clear exactly the slots compaction vacated. Sizing this from `removed` (rather than a
|
||||
// fixed one) keeps it inside the vector when nothing matched and the store is full.
|
||||
const size_t first = numMeshNodes;
|
||||
const size_t last = std::min(first + removed, nodeDatabase.nodes.size());
|
||||
std::fill(nodeDatabase.nodes.begin() + first, nodeDatabase.nodes.begin() + last, meshtastic_NodeInfoLite());
|
||||
}
|
||||
// Drop the node's satellite stores and warm-tier copy regardless of which tier it lived in, so
|
||||
// an explicit removal fully forgets it.
|
||||
eraseNodeSatellites(nodeNum);
|
||||
#if WARM_NODE_COUNT > 0
|
||||
// Explicit user removal: don't let the warm tier resurrect the node
|
||||
warmStore.remove(nodeNum);
|
||||
@@ -1898,7 +1884,8 @@ void NodeDB::cleanupMeshDB()
|
||||
// Keep any key we learned (e.g. via a DM before the NodeInfo
|
||||
// exchange completed) rather than losing it with the purge.
|
||||
if (n.public_key.size == 32)
|
||||
warmStore.absorb(gone, n.last_heard, n.public_key.bytes, n.role, warmProtectedCategory(n));
|
||||
warmStore.absorb(gone, n.last_heard, n.public_key.bytes, n.role, warmProtectedCategory(n),
|
||||
nodeInfoLiteHasXeddsaSigned(&n));
|
||||
#endif
|
||||
|
||||
eraseNodeSatellites(gone);
|
||||
@@ -2082,7 +2069,7 @@ void NodeDB::demoteOldestHotNodesToWarm()
|
||||
// Keep the public key if we have one (40 B warm record); keyless nodes
|
||||
// still get a placeholder so re-admission restores last_heard.
|
||||
warmStore.absorb(n.num, n.last_heard, n.public_key.size > 0 ? n.public_key.bytes : nullptr, n.role,
|
||||
warmProtectedCategory(n));
|
||||
warmProtectedCategory(n), nodeInfoLiteHasXeddsaSigned(&n));
|
||||
// Demotion drops the node from the header table, so drop its satellites
|
||||
// too (the eviction chokepoint) - they'd otherwise orphan until the next
|
||||
// enforceSatelliteCaps pass.
|
||||
@@ -3084,14 +3071,12 @@ HopStartStatus classifyHopStart(const meshtastic_MeshPacket &p)
|
||||
return HopStartStatus::INVALID;
|
||||
|
||||
if (p.hop_start == 0) {
|
||||
// hop_start == hop_limit == 0: intentional zero-hop broadcast (e.g. beacon). Valid by definition -
|
||||
// the packet was never meant to travel any hops, so no hop_start ambiguity applies.
|
||||
if (p.hop_limit == 0)
|
||||
return HopStartStatus::VALID;
|
||||
// Firmware prior to 2.3.0 (585805c) lacked a hop_start field. Firmware version 2.5.0 (bf34329) introduced a
|
||||
// bitfield that is always present. Use the presence of the bitfield to determine if the origin's firmware
|
||||
// version is guaranteed to have hop_start populated. Note that this can only be done for decoded packets as
|
||||
// the bitfield is encrypted under the channel encryption key.
|
||||
// hop_start == 0 is either a modern zero-hop broadcast (e.g. beacon) or pre-2.3.0 firmware (585805c)
|
||||
// that never populated hop_start. Firmware 2.5.0 (bf34329) introduced a bitfield that is always
|
||||
// present; use it to tell the two apart. The bitfield is encrypted under the channel key, so this can
|
||||
// only be resolved for decoded packets - until then the status stays MISSING_OR_UNKNOWN. Callers
|
||||
// acting before decode must therefore not treat UNKNOWN as a drop (the bitfield isn't readable yet);
|
||||
// the verdict is re-checked post-decode in Router::handleReceived.
|
||||
if (p.which_payload_variant == meshtastic_MeshPacket_decoded_tag && p.decoded.has_bitfield)
|
||||
return HopStartStatus::VALID;
|
||||
return HopStartStatus::MISSING_OR_UNKNOWN;
|
||||
@@ -3285,6 +3270,9 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact)
|
||||
LOG_WARN(PROTECTED_CAP_WARN_FMT, "ignore", contact.node_num, MAX_NUM_NODES - 2);
|
||||
nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_IS_FAVORITE_MASK, false);
|
||||
eraseNodeSatellites(contact.node_num);
|
||||
#if HAS_SCREEN || defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS)
|
||||
messageStore.deleteAllMessagesFromNode(contact.node_num);
|
||||
#endif
|
||||
} else {
|
||||
/* Clients are sending add_contact before every text message DM (because clients may hold a larger node database with
|
||||
* public keys than the radio holds). However, we don't want to update last_heard just because we sent someone a DM!
|
||||
@@ -3788,7 +3776,7 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
|
||||
// Demote to the warm tier so the identity (and crucially the
|
||||
// PKI key) outlives the hot-store slot.
|
||||
warmStore.absorb(evicted.num, evicted.last_heard, evicted.public_key.size == 32 ? evicted.public_key.bytes : NULL,
|
||||
evicted.role, warmProtectedCategory(evicted));
|
||||
evicted.role, warmProtectedCategory(evicted), nodeInfoLiteHasXeddsaSigned(&evicted));
|
||||
#endif
|
||||
eraseNodeSatellites(evicted.num);
|
||||
// Shove the remaining nodes down the chain
|
||||
@@ -3817,10 +3805,13 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
|
||||
// Re-admission: restore what the warm tier kept for this node
|
||||
WarmNodeEntry warm;
|
||||
if (warmStore.take(n, warm)) {
|
||||
lite->last_heard = warmTimeOf(warm); // mask off the stolen role/protected metadata bits
|
||||
lite->last_heard = warmTimeOf(warm); // mask off the stolen metadata bits
|
||||
// Restore the role the warm tier cached, so re-admission isn't stuck at CLIENT
|
||||
// until the next NodeInfo arrives.
|
||||
lite->role = static_cast<meshtastic_Config_DeviceConfig_Role>(warmRoleOf(warm));
|
||||
// Restore the signer bit too: it is learned from verified traffic, not from
|
||||
// NodeInfo, so a round trip through the warm tier must not relearn it from zero.
|
||||
nodeInfoLiteSetBit(lite, NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_MASK, warmSignerOf(warm));
|
||||
if (!memfll(warm.public_key, 0, sizeof(warm.public_key))) {
|
||||
lite->public_key.size = 32;
|
||||
memcpy(lite->public_key.bytes, warm.public_key, 32);
|
||||
|
||||
+4
-1
@@ -156,7 +156,10 @@ inline bool shouldDropPacketForPreHop(const meshtastic_MeshPacket &p)
|
||||
if (isFromUs(&p)) {
|
||||
return false; // local-originated packets should never be dropped by pre-hop drop policy
|
||||
}
|
||||
return classifyHopStart(p) != HopStartStatus::VALID;
|
||||
// Pre-decode: the channel-encrypted bitfield isn't readable yet, so a missing/unknown hop_start can't be
|
||||
// distinguished from a modern packet. Only drop the provably-corrupt case here; the bitfield-dependent
|
||||
// verdict is re-checked post-decode in Router::handleReceived.
|
||||
return classifyHopStart(p) == HopStartStatus::INVALID;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -1,253 +0,0 @@
|
||||
#include "PacketCache.h"
|
||||
#include "Router.h"
|
||||
|
||||
PacketCache packetCache{};
|
||||
|
||||
/**
|
||||
* Allocate a new cache entry and copy the packet header and payload into it
|
||||
*/
|
||||
PacketCacheEntry *PacketCache::cache(const meshtastic_MeshPacket *p, bool preserveMetadata)
|
||||
{
|
||||
size_t payload_size =
|
||||
(p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag) ? p->encrypted.size : p->decoded.payload.size;
|
||||
PacketCacheEntry *e = (PacketCacheEntry *)malloc(sizeof(PacketCacheEntry) + payload_size +
|
||||
(preserveMetadata ? sizeof(PacketCacheMetadata) : 0));
|
||||
if (!e) {
|
||||
LOG_ERROR("Unable to allocate memory for packet cache entry");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
*e = {};
|
||||
e->header.from = p->from;
|
||||
e->header.to = p->to;
|
||||
e->header.id = p->id;
|
||||
e->header.channel = p->channel;
|
||||
e->header.next_hop = p->next_hop;
|
||||
e->header.relay_node = p->relay_node;
|
||||
e->header.flags = (p->hop_limit & PACKET_FLAGS_HOP_LIMIT_MASK) | (p->want_ack ? PACKET_FLAGS_WANT_ACK_MASK : 0) |
|
||||
(p->via_mqtt ? PACKET_FLAGS_VIA_MQTT_MASK : 0) |
|
||||
((p->hop_start << PACKET_FLAGS_HOP_START_SHIFT) & PACKET_FLAGS_HOP_START_MASK);
|
||||
|
||||
PacketCacheMetadata m{};
|
||||
if (preserveMetadata) {
|
||||
e->has_metadata = true;
|
||||
m.rx_rssi = (uint8_t)(p->rx_rssi + 200);
|
||||
m.rx_snr = (uint8_t)((p->rx_snr + 30.0f) / 0.25f);
|
||||
m.rx_time = p->rx_time;
|
||||
m.transport_mechanism = p->transport_mechanism;
|
||||
m.priority = p->priority;
|
||||
}
|
||||
if (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag) {
|
||||
e->encrypted = true;
|
||||
e->payload_len = p->encrypted.size;
|
||||
memcpy(((unsigned char *)e) + sizeof(PacketCacheEntry), p->encrypted.bytes, p->encrypted.size);
|
||||
} else if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
|
||||
e->encrypted = false;
|
||||
if (preserveMetadata) {
|
||||
m.portnum = p->decoded.portnum;
|
||||
m.want_response = p->decoded.want_response;
|
||||
m.emoji = p->decoded.emoji;
|
||||
m.bitfield = p->decoded.bitfield;
|
||||
if (p->decoded.reply_id)
|
||||
m.reply_id = p->decoded.reply_id;
|
||||
else if (p->decoded.request_id)
|
||||
m.request_id = p->decoded.request_id;
|
||||
}
|
||||
e->payload_len = p->decoded.payload.size;
|
||||
memcpy(((unsigned char *)e) + sizeof(PacketCacheEntry), p->decoded.payload.bytes, p->decoded.payload.size);
|
||||
} else {
|
||||
LOG_ERROR("Unable to cache packet with unknown payload type %d", p->which_payload_variant);
|
||||
free(e);
|
||||
return NULL;
|
||||
}
|
||||
if (preserveMetadata)
|
||||
memcpy(((unsigned char *)e) + sizeof(PacketCacheEntry) + e->payload_len, &m, sizeof(m));
|
||||
|
||||
size += sizeof(PacketCacheEntry) + e->payload_len + (e->has_metadata ? sizeof(PacketCacheMetadata) : 0);
|
||||
insert(e);
|
||||
return e;
|
||||
};
|
||||
|
||||
/**
|
||||
* Dump a list of packets into the provided buffer
|
||||
*/
|
||||
void PacketCache::dump(void *dest, const PacketCacheEntry **entries, size_t num_entries)
|
||||
{
|
||||
unsigned char *pos = (unsigned char *)dest;
|
||||
for (size_t i = 0; i < num_entries; i++) {
|
||||
size_t entry_len =
|
||||
sizeof(PacketCacheEntry) + entries[i]->payload_len + (entries[i]->has_metadata ? sizeof(PacketCacheMetadata) : 0);
|
||||
memcpy(pos, entries[i], entry_len);
|
||||
pos += entry_len;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the length of buffer needed to dump the specified entries
|
||||
*/
|
||||
size_t PacketCache::dumpSize(const PacketCacheEntry **entries, size_t num_entries)
|
||||
{
|
||||
size_t total_size = 0;
|
||||
for (size_t i = 0; i < num_entries; i++) {
|
||||
total_size += sizeof(PacketCacheEntry) + entries[i]->payload_len;
|
||||
if (entries[i]->has_metadata)
|
||||
total_size += sizeof(PacketCacheMetadata);
|
||||
}
|
||||
return total_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a packet in the cache
|
||||
*/
|
||||
PacketCacheEntry *PacketCache::find(NodeNum from, PacketId id)
|
||||
{
|
||||
uint16_t h = PACKET_HASH(from, id);
|
||||
PacketCacheEntry *e = buckets[PACKET_CACHE_BUCKET(h)];
|
||||
while (e) {
|
||||
if (e->header.from == from && e->header.id == id)
|
||||
return e;
|
||||
e = e->next;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a packet in the cache by its hash
|
||||
*/
|
||||
PacketCacheEntry *PacketCache::find(PacketHash h)
|
||||
{
|
||||
PacketCacheEntry *e = buckets[PACKET_CACHE_BUCKET(h)];
|
||||
while (e) {
|
||||
if (PACKET_HASH(e->header.from, e->header.id) == h)
|
||||
return e;
|
||||
e = e->next;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a list of packets from the provided buffer
|
||||
*/
|
||||
bool PacketCache::load(void *src, PacketCacheEntry **entries, size_t num_entries)
|
||||
{
|
||||
memset(entries, 0, sizeof(PacketCacheEntry *) * num_entries);
|
||||
unsigned char *pos = (unsigned char *)src;
|
||||
for (size_t i = 0; i < num_entries; i++) {
|
||||
PacketCacheEntry e{};
|
||||
memcpy(&e, pos, sizeof(PacketCacheEntry));
|
||||
size_t entry_len = sizeof(PacketCacheEntry) + e.payload_len + (e.has_metadata ? sizeof(PacketCacheMetadata) : 0);
|
||||
entries[i] = (PacketCacheEntry *)malloc(entry_len);
|
||||
size += entry_len;
|
||||
if (!entries[i]) {
|
||||
LOG_ERROR("Unable to allocate memory for packet cache entry");
|
||||
for (size_t j = 0; j < i; j++) {
|
||||
size -= sizeof(PacketCacheEntry) + entries[j]->payload_len +
|
||||
(entries[j]->has_metadata ? sizeof(PacketCacheMetadata) : 0);
|
||||
free(entries[j]);
|
||||
entries[j] = NULL;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
memcpy(entries[i], pos, entry_len);
|
||||
pos += entry_len;
|
||||
}
|
||||
for (size_t i = 0; i < num_entries; i++)
|
||||
insert(entries[i]);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the cached packet into the provided MeshPacket structure
|
||||
*/
|
||||
void PacketCache::rehydrate(const PacketCacheEntry *e, meshtastic_MeshPacket *p)
|
||||
{
|
||||
if (!e || !p)
|
||||
return;
|
||||
|
||||
*p = {};
|
||||
p->from = e->header.from;
|
||||
p->to = e->header.to;
|
||||
p->id = e->header.id;
|
||||
p->channel = e->header.channel;
|
||||
p->next_hop = e->header.next_hop;
|
||||
p->relay_node = e->header.relay_node;
|
||||
p->hop_limit = e->header.flags & PACKET_FLAGS_HOP_LIMIT_MASK;
|
||||
p->want_ack = !!(e->header.flags & PACKET_FLAGS_WANT_ACK_MASK);
|
||||
p->via_mqtt = !!(e->header.flags & PACKET_FLAGS_VIA_MQTT_MASK);
|
||||
p->hop_start = (e->header.flags & PACKET_FLAGS_HOP_START_MASK) >> PACKET_FLAGS_HOP_START_SHIFT;
|
||||
p->which_payload_variant = e->encrypted ? meshtastic_MeshPacket_encrypted_tag : meshtastic_MeshPacket_decoded_tag;
|
||||
|
||||
unsigned char *payload = ((unsigned char *)e) + sizeof(PacketCacheEntry);
|
||||
PacketCacheMetadata m{};
|
||||
if (e->has_metadata) {
|
||||
memcpy(&m, (payload + e->payload_len), sizeof(m));
|
||||
p->rx_rssi = ((int)m.rx_rssi) - 200;
|
||||
p->rx_snr = ((float)m.rx_snr * 0.25f) - 30.0f;
|
||||
p->rx_time = m.rx_time;
|
||||
p->transport_mechanism = (meshtastic_MeshPacket_TransportMechanism)m.transport_mechanism;
|
||||
p->priority = (meshtastic_MeshPacket_Priority)m.priority;
|
||||
}
|
||||
if (e->encrypted) {
|
||||
memcpy(p->encrypted.bytes, payload, e->payload_len);
|
||||
p->encrypted.size = e->payload_len;
|
||||
} else {
|
||||
memcpy(p->decoded.payload.bytes, payload, e->payload_len);
|
||||
p->decoded.payload.size = e->payload_len;
|
||||
if (e->has_metadata) {
|
||||
// Decrypted-only metadata
|
||||
p->decoded.portnum = (meshtastic_PortNum)m.portnum;
|
||||
p->decoded.want_response = m.want_response;
|
||||
p->decoded.emoji = m.emoji;
|
||||
p->decoded.bitfield = m.bitfield;
|
||||
if (m.reply_id)
|
||||
p->decoded.reply_id = m.reply_id;
|
||||
else if (m.request_id)
|
||||
p->decoded.request_id = m.request_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Release a cache entry
|
||||
*/
|
||||
void PacketCache::release(PacketCacheEntry *e)
|
||||
{
|
||||
if (!e)
|
||||
return;
|
||||
remove(e);
|
||||
size -= sizeof(PacketCacheEntry) + e->payload_len + (e->has_metadata ? sizeof(PacketCacheMetadata) : 0);
|
||||
free(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a new entry into the hash table
|
||||
*/
|
||||
void PacketCache::insert(PacketCacheEntry *e)
|
||||
{
|
||||
assert(e);
|
||||
PacketHash h = PACKET_HASH(e->header.from, e->header.id);
|
||||
PacketCacheEntry **target = &buckets[PACKET_CACHE_BUCKET(h)];
|
||||
e->next = *target;
|
||||
*target = e;
|
||||
num_entries++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an entry from the hash table
|
||||
*/
|
||||
void PacketCache::remove(PacketCacheEntry *e)
|
||||
{
|
||||
assert(e);
|
||||
PacketHash h = PACKET_HASH(e->header.from, e->header.id);
|
||||
PacketCacheEntry **target = &buckets[PACKET_CACHE_BUCKET(h)];
|
||||
while (*target) {
|
||||
if (*target == e) {
|
||||
*target = e->next;
|
||||
e->next = NULL;
|
||||
num_entries--;
|
||||
break;
|
||||
} else {
|
||||
target = &(*target)->next;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
#pragma once
|
||||
#include "RadioInterface.h"
|
||||
|
||||
#define PACKET_HASH(a, b) ((((a ^ b) >> 16) ^ (a ^ b)) & 0xFFFF) // 16 bit fold of packet (from, id) tuple
|
||||
typedef uint16_t PacketHash;
|
||||
|
||||
#define PACKET_CACHE_BUCKETS 64 // Number of hash table buckets
|
||||
#define PACKET_CACHE_BUCKET(h) (((h >> 12) ^ (h >> 6) ^ h) & 0x3F) // Fold hash down to 6-bit bucket index
|
||||
|
||||
typedef struct PacketCacheEntry {
|
||||
PacketCacheEntry *next;
|
||||
PacketHeader header;
|
||||
uint16_t payload_len = 0;
|
||||
union {
|
||||
uint16_t bitfield;
|
||||
struct {
|
||||
uint8_t encrypted : 1; // Payload is encrypted
|
||||
uint8_t has_metadata : 1; // Payload includes PacketCacheMetadata
|
||||
uint8_t : 6; // Reserved for future use
|
||||
uint8_t : 8; // Reserved for future use
|
||||
};
|
||||
};
|
||||
} PacketCacheEntry;
|
||||
|
||||
typedef struct PacketCacheMetadata {
|
||||
PacketCacheMetadata() : _bitfield(0), reply_id(0), _bitfield2(0) {}
|
||||
union {
|
||||
uint32_t _bitfield;
|
||||
struct {
|
||||
uint16_t portnum : 9; // meshtastic_MeshPacket::decoded::portnum
|
||||
uint16_t want_response : 1; // meshtastic_MeshPacket::decoded::want_response
|
||||
uint16_t emoji : 1; // meshtastic_MeshPacket::decoded::emoji
|
||||
uint16_t bitfield : 5; // meshtastic_MeshPacket::decoded::bitfield (truncated)
|
||||
uint8_t rx_rssi : 8; // meshtastic_MeshPacket::rx_rssi (map via actual RSSI + 200)
|
||||
uint8_t rx_snr : 8; // meshtastic_MeshPacket::rx_snr (map via (p->rx_snr + 30.0f) / 0.25f)
|
||||
};
|
||||
};
|
||||
union {
|
||||
uint32_t reply_id; // meshtastic_MeshPacket::decoded.reply_id
|
||||
uint32_t request_id; // meshtastic_MeshPacket::decoded.request_id
|
||||
};
|
||||
uint32_t rx_time = 0; // meshtastic_MeshPacket::rx_time
|
||||
uint8_t transport_mechanism = 0; // meshtastic_MeshPacket::transport_mechanism
|
||||
struct {
|
||||
uint8_t _bitfield2;
|
||||
union {
|
||||
uint8_t priority : 7; // meshtastic_MeshPacket::priority
|
||||
uint8_t reserved : 1; // Reserved for future use
|
||||
};
|
||||
};
|
||||
} PacketCacheMetadata;
|
||||
|
||||
class PacketCache
|
||||
{
|
||||
public:
|
||||
PacketCacheEntry *cache(const meshtastic_MeshPacket *p, bool preserveMetadata);
|
||||
static void dump(void *dest, const PacketCacheEntry **entries, size_t num_entries);
|
||||
size_t dumpSize(const PacketCacheEntry **entries, size_t num_entries);
|
||||
PacketCacheEntry *find(NodeNum from, PacketId id);
|
||||
PacketCacheEntry *find(PacketHash h);
|
||||
bool load(void *src, PacketCacheEntry **entries, size_t num_entries);
|
||||
size_t getNumEntries() { return num_entries; }
|
||||
size_t getSize() { return size; }
|
||||
void rehydrate(const PacketCacheEntry *e, meshtastic_MeshPacket *p);
|
||||
void release(PacketCacheEntry *e);
|
||||
|
||||
private:
|
||||
PacketCacheEntry *buckets[PACKET_CACHE_BUCKETS]{};
|
||||
size_t num_entries = 0;
|
||||
size_t size = 0;
|
||||
void insert(PacketCacheEntry *e);
|
||||
void remove(PacketCacheEntry *e);
|
||||
};
|
||||
|
||||
extern PacketCache packetCache;
|
||||
+81
-31
@@ -38,7 +38,7 @@
|
||||
#include "mqtt/MQTT.h"
|
||||
#endif
|
||||
#include "Throttle.h"
|
||||
#include <RTC.h>
|
||||
#include "gps/RTC.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -1201,19 +1201,49 @@ void PhoneAPI::prefetchNodeInfos()
|
||||
onNowHasData(0);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
/// Derive a stable id for a replayed satellite-DB record. Unchanged history replayed on
|
||||
/// every reconnect must not look like a new packet to the phone's history/dedup - the id
|
||||
/// only changes when the underlying data (its timestamp) actually changes.
|
||||
// `kind` only needs to distinguish the record types that can otherwise collide (e.g. device
|
||||
// vs. environment metrics both replay as TELEMETRY_APP with the same node/last_heard) - pass
|
||||
// the payload's own variant/port constant.
|
||||
uint32_t makeReplayPacketId(NodeNum num, uint32_t timestamp, uint32_t kind)
|
||||
{
|
||||
uint32_t h = num;
|
||||
h = h * 2654435761u + timestamp;
|
||||
h = h * 2654435761u + kind;
|
||||
return h ? h : 1; // some clients treat id 0 as "unset"
|
||||
}
|
||||
|
||||
/// Populate hop_start/hop_limit from the node's real last-known hop count (if any) so a
|
||||
/// replayed packet doesn't read as "heard directly" when it wasn't.
|
||||
void setReplayHopFields(meshtastic_MeshPacket &pkt, const meshtastic_NodeInfoLite *header)
|
||||
{
|
||||
uint8_t hopLimit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
||||
uint8_t hopsAway = (header && header->has_hops_away) ? header->hops_away : 0;
|
||||
pkt.hop_start = hopLimit;
|
||||
pkt.hop_limit = hopsAway < hopLimit ? (uint8_t)(hopLimit - hopsAway) : 0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
meshtastic_MeshPacket PhoneAPI::makeReplayPositionPacket(NodeNum num, const meshtastic_PositionLite &pos)
|
||||
{
|
||||
// Shape this exactly like a fresh live broadcast Position from the peer so the
|
||||
// phone runs it through its normal "live position broadcast" handler path.
|
||||
// to=ourNum would read as a DM-from-peer and never lands in node detail UI.
|
||||
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default;
|
||||
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
||||
pkt.from = num;
|
||||
pkt.to = NODENUM_BROADCAST;
|
||||
pkt.id = generatePacketId();
|
||||
pkt.rx_time = pos.time;
|
||||
// Stable per-node/per-fix id: replaying the same unchanged history on every
|
||||
// reconnect must not look like a brand new packet to the phone's history/dedup.
|
||||
pkt.id = makeReplayPacketId(num, pkt.rx_time, meshtastic_PortNum_POSITION_APP);
|
||||
pkt.channel = 0;
|
||||
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
||||
pkt.hop_start = pkt.hop_limit;
|
||||
pkt.rx_snr = header ? header->snr : 0;
|
||||
setReplayHopFields(pkt, header);
|
||||
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
// Mark as if heard over the air, not internally generated
|
||||
pkt.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
|
||||
@@ -1231,13 +1261,13 @@ meshtastic_MeshPacket PhoneAPI::makeReplayTelemetryPacket(NodeNum num, const mes
|
||||
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default;
|
||||
pkt.from = num;
|
||||
pkt.to = NODENUM_BROADCAST;
|
||||
pkt.id = generatePacketId();
|
||||
// No native timestamp on telemetry packets here; use last_heard.
|
||||
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
||||
pkt.rx_time = header ? header->last_heard : 0;
|
||||
pkt.id = makeReplayPacketId(num, pkt.rx_time, meshtastic_Telemetry_device_metrics_tag);
|
||||
pkt.channel = 0;
|
||||
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
||||
pkt.hop_start = pkt.hop_limit;
|
||||
pkt.rx_snr = header ? header->snr : 0;
|
||||
setReplayHopFields(pkt, header);
|
||||
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
// Mark as if heard over the air, not internally generated - iOS client filters
|
||||
// TRANSPORT_INTERNAL packets out of broadcast peer state updates.
|
||||
@@ -1337,12 +1367,12 @@ meshtastic_MeshPacket PhoneAPI::makeReplayEnvironmentPacket(uint32_t num, const
|
||||
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default;
|
||||
pkt.from = num;
|
||||
pkt.to = NODENUM_BROADCAST;
|
||||
pkt.id = generatePacketId();
|
||||
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
||||
pkt.rx_time = header ? header->last_heard : 0;
|
||||
pkt.id = makeReplayPacketId(num, pkt.rx_time, meshtastic_Telemetry_environment_metrics_tag);
|
||||
pkt.channel = 0;
|
||||
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
||||
pkt.hop_start = pkt.hop_limit;
|
||||
pkt.rx_snr = header ? header->snr : 0;
|
||||
setReplayHopFields(pkt, header);
|
||||
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
// Mark as if heard over the air, not internally generated - iOS client filters
|
||||
// TRANSPORT_INTERNAL packets out of broadcast peer state updates.
|
||||
@@ -1400,13 +1430,13 @@ meshtastic_MeshPacket PhoneAPI::makeReplayStatusPacket(uint32_t num, const mesht
|
||||
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default;
|
||||
pkt.from = num;
|
||||
pkt.to = NODENUM_BROADCAST;
|
||||
pkt.id = generatePacketId();
|
||||
// StatusMessage has no native timestamp; use last_heard.
|
||||
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
||||
pkt.rx_time = header ? header->last_heard : 0;
|
||||
pkt.id = makeReplayPacketId(num, pkt.rx_time, meshtastic_PortNum_NODE_STATUS_APP);
|
||||
pkt.channel = 0;
|
||||
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
||||
pkt.hop_start = pkt.hop_limit;
|
||||
pkt.rx_snr = header ? header->snr : 0;
|
||||
setReplayHopFields(pkt, header);
|
||||
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
// Mark as if heard over the air, not internally generated - client filters
|
||||
pkt.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
|
||||
@@ -1667,6 +1697,19 @@ bool PhoneAPI::wasSeenRecently(uint32_t id)
|
||||
return false;
|
||||
}
|
||||
|
||||
PhoneAPI::LocalAdminGate PhoneAPI::classifyLocalAdminPacket(const meshtastic_MeshPacket &p, bool adminAuthorized,
|
||||
meshtastic_AdminMessage &outAdmin)
|
||||
{
|
||||
if (p.which_payload_variant != meshtastic_MeshPacket_decoded_tag || p.decoded.portnum != meshtastic_PortNum_ADMIN_APP)
|
||||
return LocalAdminGate::NotAdmin;
|
||||
outAdmin = meshtastic_AdminMessage_init_zero;
|
||||
if (!pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_AdminMessage_msg, &outAdmin))
|
||||
return LocalAdminGate::NotAdmin; // undecodable: let the normal reject path respond
|
||||
if (outAdmin.which_payload_variant == meshtastic_AdminMessage_lockdown_auth_tag)
|
||||
return LocalAdminGate::LockdownAuth; // the passphrase itself; authenticate regardless of prior state
|
||||
return adminAuthorized ? LocalAdminGate::AuthorizedPassThrough : LocalAdminGate::DropUnauthorized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a packet that the phone wants us to send. It is our responsibility to free the packet to the pool
|
||||
*/
|
||||
@@ -1691,26 +1734,33 @@ bool PhoneAPI::handleToRadioPacket(meshtastic_MeshPacket &p)
|
||||
// the gate here closes that race and covers H6/H7 from the
|
||||
// audit: get_config_request and set_config from unauthed
|
||||
// clients no longer reach AdminModule at all.
|
||||
if (p.from == 0 && p.which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
|
||||
p.decoded.portnum == meshtastic_PortNum_ADMIN_APP) {
|
||||
// Gate on the connection, not the wire `from`, which a client can forge to a non-zero value to
|
||||
// bypass the check before MeshService normalizes it back to a local identity.
|
||||
// Scope the decoded admin message: it holds the plaintext passphrase, so bound its lifetime.
|
||||
{
|
||||
meshtastic_AdminMessage admin = meshtastic_AdminMessage_init_zero;
|
||||
if (pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_AdminMessage_msg, &admin)) {
|
||||
if (admin.which_payload_variant == meshtastic_AdminMessage_lockdown_auth_tag) {
|
||||
handleLockdownAuthInline(admin.lockdown_auth);
|
||||
// Wipe the decoded passphrase scratch - the byte array in
|
||||
// p.decoded.payload.bytes is wiped by handleLockdownAuthInline.
|
||||
volatile uint8_t *adminVol = const_cast<volatile uint8_t *>(admin.lockdown_auth.passphrase.bytes);
|
||||
for (size_t i = 0; i < sizeof(admin.lockdown_auth.passphrase.bytes); i++)
|
||||
adminVol[i] = 0;
|
||||
return true;
|
||||
}
|
||||
if (!getAdminAuthorized()) {
|
||||
LOG_WARN("Lockdown: dropping admin payload variant=%d from unauthorized connection", admin.which_payload_variant);
|
||||
return false;
|
||||
}
|
||||
switch (classifyLocalAdminPacket(p, getAdminAuthorized(), admin)) {
|
||||
case LocalAdminGate::LockdownAuth: {
|
||||
handleLockdownAuthInline(admin.lockdown_auth);
|
||||
// The encoded wipe is the security-critical one: nothing else clears the passphrase from
|
||||
// the packet buffer. handleLockdownAuthInline already zeroes the decoded copy up to its
|
||||
// size; re-zero the full scratch capacity here as defense in depth.
|
||||
volatile uint8_t *adminVol = const_cast<volatile uint8_t *>(admin.lockdown_auth.passphrase.bytes);
|
||||
for (size_t i = 0; i < sizeof(admin.lockdown_auth.passphrase.bytes); i++)
|
||||
adminVol[i] = 0;
|
||||
volatile uint8_t *encodedVol = const_cast<volatile uint8_t *>(p.decoded.payload.bytes);
|
||||
for (size_t i = 0; i < sizeof(p.decoded.payload.bytes); i++)
|
||||
encodedVol[i] = 0;
|
||||
p.decoded.payload.size = 0; // keep the length consistent with the wiped buffer
|
||||
return true;
|
||||
}
|
||||
case LocalAdminGate::DropUnauthorized:
|
||||
LOG_WARN("Lockdown: dropping admin payload variant=%d from unauthorized connection", admin.which_payload_variant);
|
||||
return false;
|
||||
case LocalAdminGate::NotAdmin:
|
||||
case LocalAdminGate::AuthorizedPassThrough:
|
||||
break; // normal handling
|
||||
}
|
||||
// pb_decode failure: fall through to normal handling so the
|
||||
// regular Router/AdminModule reject path can respond.
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -319,4 +319,18 @@ class PhoneAPI
|
||||
|
||||
/// If the mesh service tells us fromNum has changed, tell the phone
|
||||
virtual int onNotify(uint32_t newValue) override;
|
||||
|
||||
public:
|
||||
/// How the lockdown admin gate should treat a phone->radio packet.
|
||||
enum class LocalAdminGate {
|
||||
NotAdmin, ///< Not a decodable ADMIN_APP payload; normal handling.
|
||||
LockdownAuth, ///< A lockdown_auth payload; authenticate the connection inline.
|
||||
DropUnauthorized, ///< Admin payload from a connection that has not authenticated; drop.
|
||||
AuthorizedPassThrough, ///< Admin payload from an authorized connection; normal handling.
|
||||
};
|
||||
|
||||
/// Classify a phone->radio packet for the lockdown admin gate, ignoring the wire `from` (which a
|
||||
/// client can forge) and deciding on the connection's authorization. Fills outAdmin for lockdown.
|
||||
static LocalAdminGate classifyLocalAdminPacket(const meshtastic_MeshPacket &p, bool adminAuthorized,
|
||||
meshtastic_AdminMessage &outAdmin);
|
||||
};
|
||||
|
||||
+43
-21
@@ -39,8 +39,8 @@
|
||||
#endif
|
||||
|
||||
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_STD[] = {
|
||||
PRESET(LONG_FAST), PRESET(LONG_SLOW), PRESET(MEDIUM_SLOW), PRESET(MEDIUM_FAST), PRESET(SHORT_SLOW),
|
||||
PRESET(SHORT_FAST), PRESET(LONG_MODERATE), PRESET(SHORT_TURBO), PRESET(LONG_TURBO), MODEM_PRESET_END};
|
||||
PRESET(LONG_FAST), PRESET(LONG_SLOW), PRESET(MEDIUM_SLOW), PRESET(MEDIUM_FAST), PRESET(SHORT_SLOW), PRESET(SHORT_FAST),
|
||||
PRESET(LONG_MODERATE), PRESET(SHORT_TURBO), PRESET(LONG_TURBO), PRESET(MEDIUM_TURBO), MODEM_PRESET_END};
|
||||
|
||||
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_EU_868[] = {
|
||||
PRESET(LONG_FAST), PRESET(LONG_SLOW), PRESET(MEDIUM_SLOW), PRESET(MEDIUM_FAST),
|
||||
@@ -55,6 +55,34 @@ static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_NARROW[] = {PRESET
|
||||
|
||||
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_TINY[] = {PRESET(TINY_FAST), PRESET(TINY_SLOW), MODEM_PRESET_END};
|
||||
|
||||
// The EU_868/EU_866/EU_N_868 trio share the 868 band but own mutually exclusive preset
|
||||
// profiles. Selecting a preset locked to a sibling swaps the region to that sibling (see
|
||||
// regionSwapForPreset), so from any region in the trio every one of these presets is
|
||||
// selectable. This union is what we advertise to clients as the trio's legal list. It is a
|
||||
// display-only superset: on-device enforcement still uses each region's own disjoint
|
||||
// profile->presets, so this must never be assigned to a RegionProfile (that would make
|
||||
// supportsPreset() accept the preset in place and defeat the swap). Keep in sync with the
|
||||
// EU_868/EU_866/EU_N_868 profile lists below. Sized to the 11-preset wire cap.
|
||||
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_EU_SUPERSET[] = {
|
||||
PRESET(LONG_FAST), PRESET(LONG_SLOW), PRESET(MEDIUM_SLOW), PRESET(MEDIUM_FAST), PRESET(SHORT_SLOW), PRESET(SHORT_FAST),
|
||||
PRESET(LONG_MODERATE), PRESET(LITE_FAST), PRESET(LITE_SLOW), PRESET(NARROW_FAST), PRESET(NARROW_SLOW), MODEM_PRESET_END};
|
||||
|
||||
// The EU_868/EU_866/EU_N_868 trio own mutually exclusive preset lists. Selecting a preset
|
||||
// locked to a sibling means the user wants that sibling region, not the default preset.
|
||||
static const meshtastic_Config_LoRaConfig_RegionCode SWAPPABLE_EU_REGIONS[] = {
|
||||
meshtastic_Config_LoRaConfig_RegionCode_EU_868,
|
||||
meshtastic_Config_LoRaConfig_RegionCode_EU_866,
|
||||
meshtastic_Config_LoRaConfig_RegionCode_EU_N_868,
|
||||
};
|
||||
|
||||
static bool isSwappableEuRegion(meshtastic_Config_LoRaConfig_RegionCode code)
|
||||
{
|
||||
for (auto c : SWAPPABLE_EU_REGIONS)
|
||||
if (c == code)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Region profiles: bundle preset list + regulatory parameters shared across regions
|
||||
// presets, spacing, padding, audio, licensed, text throttle, position throttle, telemetry throttle
|
||||
const RegionProfile PROFILE_STD = {PRESETS_STD, 0, 0, true, false, 0, 1, 1};
|
||||
@@ -184,11 +212,9 @@ const RegionInfo regions[] = {
|
||||
|
||||
/*
|
||||
433,05-434,7 Mhz 10 mW
|
||||
868,0-868,6 Mhz 25 mW
|
||||
https://nkrzi.gov.ua/images/upload/256/5810/PDF_UUZ_19_01_2016.pdf
|
||||
https://zakon.rada.gov.ua/laws/show/262-2026-п
|
||||
*/
|
||||
RDEF(UA_433, 433.0f, 434.7f, 10, 10, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
RDEF(UA_868, 868.0f, 868.6f, 1, 14, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
|
||||
/*
|
||||
Malaysia
|
||||
@@ -337,8 +363,12 @@ LoRaRadioType radioType = NO_RADIO;
|
||||
|
||||
extern RadioLibHal *RadioLibHAL;
|
||||
#if defined(HW_SPI1_DEVICE) && defined(ARCH_ESP32)
|
||||
#if defined(HAS_SDCARD) && defined(SDCARD_USE_SPI1)
|
||||
extern SPIClass &SPI1; // alias for SPI_HSPI; both on SPI2_HOST
|
||||
#else
|
||||
extern SPIClass SPI1;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
std::unique_ptr<RadioInterface> initLoRa()
|
||||
{
|
||||
@@ -685,8 +715,13 @@ void getRegionPresetMap(meshtastic_LoRaRegionPresetMap &map)
|
||||
grp.default_preset = r->getDefaultPreset();
|
||||
grp.licensed_only = r->profile->licensedOnly;
|
||||
grp.presets_count = 0;
|
||||
for (size_t i = 0; r->profile->presets[i] != MODEM_PRESET_END && grp.presets_count < maxPresets; i++)
|
||||
grp.presets[grp.presets_count++] = r->profile->presets[i];
|
||||
// EU 86x siblings advertise the trio's superset - any of those presets is
|
||||
// reachable from here via an automatic region swap. Every other region
|
||||
// advertises exactly its own enforced profile list.
|
||||
const meshtastic_Config_LoRaConfig_ModemPreset *advertised =
|
||||
isSwappableEuRegion(r->code) ? PRESETS_EU_SUPERSET : r->profile->presets;
|
||||
for (size_t i = 0; advertised[i] != MODEM_PRESET_END && grp.presets_count < maxPresets; i++)
|
||||
grp.presets[grp.presets_count++] = advertised[i];
|
||||
}
|
||||
|
||||
// Map this region to its group (capacity checked at the top of the loop).
|
||||
@@ -961,14 +996,6 @@ static void sendErrorNotification(const char *msg, meshtastic_LogRecord_Level le
|
||||
service->sendClientNotification(cn);
|
||||
}
|
||||
|
||||
// The EU_868/EU_866/EU_N_868 trio own mutually exclusive preset lists. Selecting a preset
|
||||
// locked to a sibling means the user wants that sibling region, not the default preset.
|
||||
static const meshtastic_Config_LoRaConfig_RegionCode SWAPPABLE_EU_REGIONS[] = {
|
||||
meshtastic_Config_LoRaConfig_RegionCode_EU_868,
|
||||
meshtastic_Config_LoRaConfig_RegionCode_EU_866,
|
||||
meshtastic_Config_LoRaConfig_RegionCode_EU_N_868,
|
||||
};
|
||||
|
||||
/**
|
||||
* If currentRegion is one of the swappable EU regions and preset belongs to a sibling in
|
||||
* that trio, return the sibling region that owns the preset. Returns nullptr otherwise.
|
||||
@@ -976,12 +1003,7 @@ static const meshtastic_Config_LoRaConfig_RegionCode SWAPPABLE_EU_REGIONS[] = {
|
||||
const RegionInfo *RadioInterface::regionSwapForPreset(meshtastic_Config_LoRaConfig_RegionCode currentRegion,
|
||||
meshtastic_Config_LoRaConfig_ModemPreset preset)
|
||||
{
|
||||
bool currentIsSwappable = false;
|
||||
for (auto code : SWAPPABLE_EU_REGIONS) {
|
||||
if (code == currentRegion)
|
||||
currentIsSwappable = true;
|
||||
}
|
||||
if (!currentIsSwappable)
|
||||
if (!isSwappableEuRegion(currentRegion))
|
||||
return nullptr;
|
||||
|
||||
for (auto code : SWAPPABLE_EU_REGIONS) {
|
||||
|
||||
+74
-25
@@ -5,7 +5,7 @@
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PositionPrecision.h"
|
||||
#include "RTC.h"
|
||||
#include "gps/RTC.h"
|
||||
|
||||
#include "configuration.h"
|
||||
#include "main.h"
|
||||
@@ -711,37 +711,62 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
bool matchedChannel = false;
|
||||
ChannelIndex chIndex = 0;
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||
// Attempt PKI decryption first. The sender's key may come from the hot
|
||||
// store or the warm tier (nodes evicted from the hot store keep their key
|
||||
// there), so DMs from long-tail nodes still decrypt.
|
||||
meshtastic_NodeInfoLite_public_key_t fromKey = {0, {0}};
|
||||
if (p->channel == 0 && isToUs(p) && p->to > 0 && !isBroadcast(p->to) && nodeDB->copyPublicKey(p->from, fromKey) &&
|
||||
nodeDB->getMeshNode(p->to) != nullptr && nodeDB->getMeshNode(p->to)->public_key.size > 0 &&
|
||||
rawSize > MESHTASTIC_PKC_OVERHEAD) {
|
||||
// Resolve the sender's public key: prefer the one stored in NodeDB (hot store or warm tier), else
|
||||
// fall back to a not-yet-committed key held during an in-progress key-verification handshake.
|
||||
meshtastic_NodeInfoLite_public_key_t remotePublic = {0, {0}};
|
||||
bool haveRemoteKey = nodeDB->copyPublicKey(p->from, remotePublic) || crypto->getPendingPublicKey(p->from, remotePublic);
|
||||
|
||||
meshtastic_NodeInfoLite *ourNode = nullptr;
|
||||
if (p->channel == 0 && isToUs(p) && p->to > 0 && !isBroadcast(p->to) && rawSize > MESHTASTIC_PKC_OVERHEAD &&
|
||||
(ourNode = nodeDB->getMeshNode(p->to)) != nullptr && ourNode->public_key.size > 0) {
|
||||
pkiAttempted = true;
|
||||
LOG_DEBUG("Attempt PKI decryption");
|
||||
// Try the sender's known key first, then each configured admin key so an authorized admin can
|
||||
// reach a node that has not yet learned their key. AES-CCM AEAD rejects wrong candidates.
|
||||
bool viaAdminKey = false;
|
||||
if (haveRemoteKey && crypto->decryptCurve25519(p->from, remotePublic, p->id, rawSize, p->encrypted.bytes, bytes)) {
|
||||
decrypted = true;
|
||||
}
|
||||
for (int i = 0; i < 3 && !decrypted; i++) {
|
||||
if (config.security.admin_key[i].size != 32)
|
||||
continue;
|
||||
remotePublic.size = 32;
|
||||
memcpy(remotePublic.bytes, config.security.admin_key[i].bytes, 32);
|
||||
|
||||
if (crypto->decryptCurve25519(p->from, fromKey, p->id, rawSize, p->encrypted.bytes, bytes)) {
|
||||
if (crypto->decryptCurve25519(p->from, remotePublic, p->id, rawSize, p->encrypted.bytes, bytes)) {
|
||||
decrypted = true;
|
||||
viaAdminKey = true;
|
||||
break; // stop after first successful decryption
|
||||
}
|
||||
}
|
||||
if (decrypted) {
|
||||
LOG_INFO("PKI Decryption worked!");
|
||||
|
||||
meshtastic_Data decodedtmp;
|
||||
memset(&decodedtmp, 0, sizeof(decodedtmp));
|
||||
rawSize -= MESHTASTIC_PKC_OVERHEAD;
|
||||
if (pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp) &&
|
||||
size_t payloadSize = rawSize - MESHTASTIC_PKC_OVERHEAD;
|
||||
if (pb_decode_from_bytes(bytes, payloadSize, &meshtastic_Data_msg, &decodedtmp) &&
|
||||
decodedtmp.portnum != meshtastic_PortNum_UNKNOWN_APP) {
|
||||
decrypted = true;
|
||||
rawSize = payloadSize; // commit the overhead subtraction only on full success
|
||||
LOG_INFO("Packet decrypted using PKI!");
|
||||
p->pki_encrypted = true;
|
||||
memcpy(p->public_key.bytes, fromKey.bytes, 32);
|
||||
memcpy(p->public_key.bytes, remotePublic.bytes, 32);
|
||||
p->public_key.size = 32;
|
||||
p->decoded = decodedtmp;
|
||||
p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded
|
||||
if (viaAdminKey) {
|
||||
// Persist the admin key for the sender so future packets take the fast path and we can
|
||||
// PKI-reply; p->from is bound into the AEAD nonce, so the trusted admin authenticated it.
|
||||
meshtastic_NodeInfoLite *fromNode = nodeDB->getOrCreateMeshNode(p->from);
|
||||
if (fromNode != nullptr)
|
||||
fromNode->public_key = remotePublic;
|
||||
}
|
||||
} 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!");
|
||||
return DecodeState::DECODE_FAILURE;
|
||||
}
|
||||
} else {
|
||||
LOG_WARN("PKC decrypt attempted but failed!");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -786,17 +811,16 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
if (decrypted) {
|
||||
// parsing was successful
|
||||
p->channel = chIndex; // change to store the index instead of the hash
|
||||
if (p->decoded.has_bitfield)
|
||||
p->decoded.want_response |= p->decoded.bitfield & BITFIELD_WANT_RESPONSE_MASK;
|
||||
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA)
|
||||
// rawSize is the size of the encoded Data exactly as the sender built it (the PKI branch's
|
||||
// MESHTASTIC_PKC_OVERHEAD subtraction preserves that, and PKI packets are unicast so the
|
||||
// downgrade predicate ignores them anyway).
|
||||
// Use the encoded Data size before merging local-only bitfield state.
|
||||
if (!checkXeddsaReceivePolicy(p, rawSize))
|
||||
return DecodeState::DECODE_POLICY_REJECT;
|
||||
#endif
|
||||
|
||||
if (p->decoded.has_bitfield)
|
||||
p->decoded.want_response |= p->decoded.bitfield & BITFIELD_WANT_RESPONSE_MASK;
|
||||
|
||||
/* Not actually ever used.
|
||||
// Decompress if needed. jm
|
||||
if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP) {
|
||||
@@ -918,7 +942,7 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
|
||||
|
||||
LOG_DEBUG("Original length - %d ", p->decoded.payload.size);
|
||||
LOG_DEBUG("Compressed length - %d ", compressed_len);
|
||||
LOG_DEBUG("Original message - %s ", p->decoded.payload.bytes);
|
||||
LOG_DEBUG("Original message - %.*s ", (int)p->decoded.payload.size, p->decoded.payload.bytes);
|
||||
|
||||
// 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) {
|
||||
@@ -947,10 +971,17 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
|
||||
ChannelIndex chIndex = p->channel; // keep as a local because we are about to change it
|
||||
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||
// Destination key from the hot store or the warm tier (evicted
|
||||
// long-tail nodes keep their key there)
|
||||
// Resolve the destination's public key: prefer NodeDB (hot store or warm tier - evicted
|
||||
// long-tail nodes keep their key there), otherwise (for a key-verification follow-on packet
|
||||
// that explicitly requested PKI) fall back to the not-yet-verified key held during an
|
||||
// in-progress handshake. This lets us DH-encode the follow-on packet before the peer's key
|
||||
// has been committed to NodeDB.
|
||||
meshtastic_NodeInfoLite_public_key_t destKey = {0, {0}};
|
||||
bool haveDestKey = nodeDB->copyPublicKey(p->to, destKey);
|
||||
if (!haveDestKey && p->pki_encrypted && p->decoded.portnum == meshtastic_PortNum_KEY_VERIFICATION_APP &&
|
||||
crypto->getPendingPublicKey(p->to, destKey)) {
|
||||
haveDestKey = true;
|
||||
}
|
||||
// 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
|
||||
// First, only PKC encrypt packets we are originating
|
||||
@@ -968,11 +999,17 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
|
||||
config.security.private_key.size == 32 && !isBroadcast(p->to) &&
|
||||
// Some portnums either make no sense to send with PKC
|
||||
p->decoded.portnum != meshtastic_PortNum_TRACEROUTE_APP && p->decoded.portnum != meshtastic_PortNum_NODEINFO_APP &&
|
||||
p->decoded.portnum != meshtastic_PortNum_ROUTING_APP && p->decoded.portnum != meshtastic_PortNum_POSITION_APP) {
|
||||
p->decoded.portnum != meshtastic_PortNum_ROUTING_APP && p->decoded.portnum != meshtastic_PortNum_POSITION_APP &&
|
||||
// We allow Key Verification messages to be sent without a known destination key, since the point of those messages is
|
||||
// to exchange keys. The first exchange (no usable key yet) falls through to channel encryption; the follow-on packet
|
||||
// uses the pending key resolved into haveDestKey/destKey above.
|
||||
// Though possible the first packet each direction should go non-pkc
|
||||
// to handle the case where the remote node has our key, but we don't have theirs.
|
||||
!(p->decoded.portnum == meshtastic_PortNum_KEY_VERIFICATION_APP && !haveDestKey)) {
|
||||
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 known public key for the destination
|
||||
// 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);
|
||||
@@ -1085,6 +1122,18 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
|
||||
else
|
||||
printPacket("handleReceived(REMOTE)", p);
|
||||
|
||||
#if MESHTASTIC_PREHOP_DROP
|
||||
// Pre-hop firmware drop, post-decode half: the bitfield that proves the origin populated hop_start is
|
||||
// encrypted under the channel key, so it can only be evaluated now that the packet is decoded. A packet
|
||||
// whose hop_start is still missing/unknown comes from pre-hop firmware - keep it out of module
|
||||
// processing, admin handling, phone delivery, MQTT and rebroadcast. Local-origin packets are exempt.
|
||||
if (!isFromUs(p) && classifyHopStart(*p) != HopStartStatus::VALID) {
|
||||
logHopStartDrop(*p, "post-decode pre-hop drop");
|
||||
cancelSending(p->from, p->id);
|
||||
skipHandle = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Neighbor info module is disabled, ignore expensive neighbor info packets
|
||||
if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
|
||||
p->decoded.portnum == meshtastic_PortNum_NEIGHBORINFO_APP &&
|
||||
|
||||
@@ -173,30 +173,6 @@ template <typename T> bool SX126xInterface<T>::init()
|
||||
LOG_WARN("Failed to apply SX1262 register 0x8B5 patch for RX improvement");
|
||||
}
|
||||
|
||||
#if 0
|
||||
// Read/write a register we are not using (only used for FSK mode) to test SPI comms
|
||||
uint8_t crcLSB = 0;
|
||||
int err = lora.readRegister(SX126X_REG_CRC_POLYNOMIAL_LSB, &crcLSB, 1);
|
||||
if(err != RADIOLIB_ERR_NONE)
|
||||
RECORD_CRITICALERROR(CriticalErrorCode_SX1262Failure);
|
||||
|
||||
//if(crcLSB != 0x0f)
|
||||
// RECORD_CRITICALERROR(CriticalErrorCode_SX1262Failure);
|
||||
|
||||
crcLSB = 0x5a;
|
||||
err = lora.writeRegister(SX126X_REG_CRC_POLYNOMIAL_LSB, &crcLSB, 1);
|
||||
if(err != RADIOLIB_ERR_NONE)
|
||||
RECORD_CRITICALERROR(CriticalErrorCode_SX1262Failure);
|
||||
|
||||
err = lora.readRegister(SX126X_REG_CRC_POLYNOMIAL_LSB, &crcLSB, 1);
|
||||
if(err != RADIOLIB_ERR_NONE)
|
||||
RECORD_CRITICALERROR(CriticalErrorCode_SX1262Failure);
|
||||
|
||||
if(crcLSB != 0x5a)
|
||||
RECORD_CRITICALERROR(CriticalErrorCode_SX1262Failure);
|
||||
// If we got this far register accesses (and therefore SPI comms) are good
|
||||
#endif
|
||||
|
||||
if (res == RADIOLIB_ERR_NONE)
|
||||
res = lora.setCRC(RADIOLIB_SX126X_LORA_CRC_ON);
|
||||
|
||||
|
||||
+32
-10
@@ -1,14 +1,15 @@
|
||||
#include "StreamAPI.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "Throttle.h"
|
||||
#include "concurrency/LockGuard.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/RTC.h"
|
||||
|
||||
#define START1 0x94
|
||||
#define START2 0xc3
|
||||
#define HEADER_LEN 4
|
||||
|
||||
/// Poll the underlying stream, drain output, and update connection state.
|
||||
int32_t StreamAPI::runOncePart()
|
||||
{
|
||||
auto result = readStream();
|
||||
@@ -17,6 +18,7 @@ int32_t StreamAPI::runOncePart()
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Consume supplied input bytes, drain output, and update connection state.
|
||||
int32_t StreamAPI::runOncePart(char *buf, uint16_t bufLen)
|
||||
{
|
||||
auto result = readStream(buf, bufLen);
|
||||
@@ -48,6 +50,11 @@ int32_t StreamAPI::readStream(const char *buf, uint16_t bufLen)
|
||||
void StreamAPI::writeStream()
|
||||
{
|
||||
if (canWrite) {
|
||||
// A transport that retained a short frame must complete it before
|
||||
// getFromRadio() advances the PhoneAPI state to the next packet.
|
||||
if (!finishPendingFrame())
|
||||
return;
|
||||
|
||||
uint32_t len;
|
||||
do {
|
||||
// Send every packet we can
|
||||
@@ -58,6 +65,7 @@ void StreamAPI::writeStream()
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse supplied bytes through the framed ToRadio receive state machine.
|
||||
int32_t StreamAPI::handleRecStream(const char *buf, uint16_t bufLen)
|
||||
{
|
||||
uint16_t index = 0;
|
||||
@@ -167,20 +175,27 @@ int32_t StreamAPI::readStream()
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode the stream marker and big-endian payload length.
|
||||
size_t StreamAPI::buildFrameHeader(uint8_t *buf, size_t payloadLen)
|
||||
{
|
||||
buf[0] = START1;
|
||||
buf[1] = START2;
|
||||
buf[2] = (payloadLen >> 8) & 0xff;
|
||||
buf[3] = payloadLen & 0xff;
|
||||
return payloadLen + HEADER_LEN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the current txBuffer over our stream
|
||||
*/
|
||||
bool StreamAPI::writeFrame(uint8_t *buf, size_t len)
|
||||
/// Write one framed payload using the transport's failure semantics.
|
||||
bool StreamAPI::writeFrame(uint8_t *buf, size_t len, bool bestEffort)
|
||||
{
|
||||
(void)bestEffort;
|
||||
if (len == 0 || !canWrite)
|
||||
return false;
|
||||
|
||||
buf[0] = START1;
|
||||
buf[1] = START2;
|
||||
buf[2] = (len >> 8) & 0xff;
|
||||
buf[3] = len & 0xff;
|
||||
|
||||
auto totalLen = len + HEADER_LEN;
|
||||
const size_t totalLen = buildFrameHeader(buf, len);
|
||||
// Serialize write-readiness checks, writes and write-failure handling
|
||||
// against concurrent stream writes/close.
|
||||
concurrency::LockGuard guard(&streamLock);
|
||||
@@ -197,11 +212,13 @@ bool StreamAPI::writeFrame(uint8_t *buf, size_t len)
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Emit the prepared main PhoneAPI payload as required output.
|
||||
bool StreamAPI::emitTxBuffer(size_t len)
|
||||
{
|
||||
return writeFrame(txBuf, len);
|
||||
return writeFrame(txBuf, len, false);
|
||||
}
|
||||
|
||||
/// Emit the initial reboot notification as a framed FromRadio payload.
|
||||
void StreamAPI::emitRebooted()
|
||||
{
|
||||
// In case we send a FromRadio packet
|
||||
@@ -213,8 +230,13 @@ void StreamAPI::emitRebooted()
|
||||
emitTxBuffer(pb_encode_to_bytes(txBuf + HEADER_LEN, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratch));
|
||||
}
|
||||
|
||||
/// Encode and emit one protobuf LogRecord using the dedicated log buffers.
|
||||
void StreamAPI::emitLogRecord(meshtastic_LogRecord_Level level, const char *src, const char *format, va_list arg)
|
||||
{
|
||||
// A retained short log frame still points into txBufLog, so do not overwrite it.
|
||||
if (!canEncodeLogRecord())
|
||||
return;
|
||||
|
||||
// IMPORTANT: do NOT touch `fromRadioScratch` or `txBuf` here - those
|
||||
// belong to the main packet-emission path and a LOG_ firing during
|
||||
// `writeStream()` would corrupt an in-flight encode. We keep a
|
||||
@@ -237,7 +259,7 @@ void StreamAPI::emitLogRecord(meshtastic_LogRecord_Level level, const char *src,
|
||||
|
||||
size_t len =
|
||||
pb_encode_to_bytes(txBufLog + HEADER_LEN, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratchLog);
|
||||
writeFrame(txBufLog, len);
|
||||
writeFrame(txBufLog, len, true);
|
||||
}
|
||||
|
||||
/// Hookable to find out when connection changes
|
||||
|
||||
+14
-3
@@ -91,12 +91,24 @@ class StreamAPI : public PhoneAPI
|
||||
/// Low level function to emit a protobuf encapsulated log record
|
||||
void emitLogRecord(meshtastic_LogRecord_Level level, const char *src, const char *format, va_list arg);
|
||||
|
||||
/// Return whether the transport can accept a frame of the requested size.
|
||||
virtual bool canWriteFrame(size_t frameLen) { return true; }
|
||||
/// Let transports recover from or close after an incomplete write.
|
||||
virtual void onFrameWriteFailed(size_t frameLen, size_t writtenLen) {}
|
||||
|
||||
private:
|
||||
bool writeFrame(uint8_t *buf, size_t len);
|
||||
/// Fill in the 4-byte 0x94C3 length header; returns the total frame length.
|
||||
static size_t buildFrameHeader(uint8_t *buf, size_t payloadLen);
|
||||
|
||||
/// Complete retained transport output before dequeuing another PhoneAPI packet.
|
||||
virtual bool finishPendingFrame() { return true; }
|
||||
/// Return whether the dedicated log buffer is available for encoding.
|
||||
virtual bool canEncodeLogRecord() { return true; }
|
||||
/// Frame and write a payload, optionally using best-effort admission.
|
||||
virtual bool writeFrame(uint8_t *buf, size_t len, bool bestEffort);
|
||||
|
||||
concurrency::Lock streamLock;
|
||||
|
||||
private:
|
||||
/// Dedicated scratch + tx buffer for LogRecord emission.
|
||||
///
|
||||
/// The main packet emission path (`writeStream` -> `getFromRadio` ->
|
||||
@@ -117,5 +129,4 @@ class StreamAPI : public PhoneAPI
|
||||
/// interleave on the wire.
|
||||
meshtastic_FromRadio fromRadioScratchLog = {};
|
||||
uint8_t txBufLog[MAX_STREAM_BUF_SIZE] = {0};
|
||||
concurrency::Lock streamLock;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
#include "StreamFrameWriter.h"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
/// Start a frame or retain required output behind an incomplete frame.
|
||||
bool StreamFrameWriter::writeFrame(Stream &stream, uint8_t *frame, size_t frameLen, bool bestEffort)
|
||||
{
|
||||
if (!finishPendingFrame(stream)) {
|
||||
// Single required-frame producer invariant: writeStream() is gated on
|
||||
// finishPendingFrame(), so a second required frame can never arrive here.
|
||||
assert(bestEffort || !deferredFrame);
|
||||
|
||||
// Preserve required output that was encoded while another frame tail
|
||||
// was pending. Best-effort output is dropped instead.
|
||||
if (!bestEffort && !deferredFrame) {
|
||||
deferredFrame = frame;
|
||||
deferredFrameLen = frameLen;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Never start best-effort output unless its complete frame fits.
|
||||
if (bestEffort && stream.availableForWrite() < (int)frameLen)
|
||||
return false;
|
||||
|
||||
size_t written = stream.write(frame, frameLen);
|
||||
if (written == frameLen)
|
||||
return true;
|
||||
|
||||
pendingFrame = frame;
|
||||
pendingFrameLen = frameLen;
|
||||
pendingFrameOffset = written;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Continue the pending tail with at most one Stream::write() call.
|
||||
bool StreamFrameWriter::finishPendingFrame(Stream &stream)
|
||||
{
|
||||
if (!pendingFrame)
|
||||
return true;
|
||||
|
||||
size_t remaining = pendingFrameLen - pendingFrameOffset;
|
||||
size_t written = stream.write(pendingFrame + pendingFrameOffset, remaining);
|
||||
if (written > remaining)
|
||||
written = remaining;
|
||||
pendingFrameOffset += written;
|
||||
|
||||
if (pendingFrameOffset < pendingFrameLen)
|
||||
return false;
|
||||
|
||||
pendingFrame = nullptr;
|
||||
pendingFrameLen = 0;
|
||||
pendingFrameOffset = 0;
|
||||
|
||||
// Promote required output without starting another write in this call.
|
||||
if (deferredFrame) {
|
||||
pendingFrame = deferredFrame;
|
||||
pendingFrameLen = deferredFrameLen;
|
||||
deferredFrame = nullptr;
|
||||
deferredFrameLen = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "Stream.h"
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
/** Caller-owned frame storage must remain valid and unchanged until isIdle(). */
|
||||
class StreamFrameWriter
|
||||
{
|
||||
public:
|
||||
/// Start a complete frame, or retain a required frame behind pending output.
|
||||
bool writeFrame(Stream &stream, uint8_t *frame, size_t frameLen, bool bestEffort);
|
||||
|
||||
/// Make one bounded attempt to finish pending output.
|
||||
bool finishPendingFrame(Stream &stream);
|
||||
|
||||
/// Return true when no frame buffer is retained.
|
||||
bool isIdle() const { return pendingFrame == nullptr && deferredFrame == nullptr; }
|
||||
|
||||
private:
|
||||
uint8_t *pendingFrame = nullptr;
|
||||
size_t pendingFrameLen = 0;
|
||||
size_t pendingFrameOffset = 0;
|
||||
uint8_t *deferredFrame = nullptr;
|
||||
size_t deferredFrameLen = 0;
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "TransmitHistory.h"
|
||||
#include "FSCommon.h"
|
||||
#include "RTC.h"
|
||||
#include "SPILock.h"
|
||||
#include "gps/RTC.h"
|
||||
#include <Throttle.h>
|
||||
|
||||
#ifdef FSCom
|
||||
|
||||
+57
-39
@@ -13,12 +13,11 @@
|
||||
|
||||
#if defined(NRF52840_XXAA)
|
||||
#include "flash/flash_nrf5x.h"
|
||||
#define WARM_RING_MAGIC 0x324E5257u // "WRN2" - v2: last_heard low bits carry role + protected category
|
||||
#define WARM_RING_MAGIC 0x334E5257u // "WRN3" - v3: last_heard low bits carry role + protected + signer
|
||||
#define WARM_RING_MAGIC_V2 0x324E5257u // "WRN2" - v2: role + protected only; bit 6 was still timestamp.
|
||||
#define WARM_RING_MAGIC_V1 0x474E5257u // "WRNG" - v1: last_heard was a plain timestamp.
|
||||
// v1 pages are still read on upgrade: we keep each record's identity + public key but
|
||||
// DISCARD its last_heard (the old timestamp would be misread as role/protected bits).
|
||||
// Records re-rank and re-learn their role on the next contact. Legacy pages convert to
|
||||
// v2 naturally as the ring rotates.
|
||||
// Older pages are still read on upgrade: v1 keeps identity + key but discards last_heard,
|
||||
// v2 keeps the word but clears bit 6, which was still part of the timestamp then.
|
||||
// A tombstone is an entry record whose last_heard is all-ones - getTime()
|
||||
// (unix seconds) cannot reach 0xFFFFFFFF until 2106, and erased flash is
|
||||
// detected via num == 0xFFFFFFFF before last_heard is ever inspected.
|
||||
@@ -34,10 +33,13 @@ struct WarmStoreHeader {
|
||||
};
|
||||
static_assert(sizeof(WarmStoreHeader) == 16, "header layout is part of the persistence format");
|
||||
|
||||
#define WARM_STORE_MAGIC 0x324D5257u // "WRM2" - v2: last_heard low bits carry role + protected category
|
||||
#define WARM_STORE_MAGIC 0x334D5257u // "WRM3" - v3: last_heard low bits carry role + protected + signer
|
||||
#define WARM_STORE_MAGIC_V2 \
|
||||
0x324D5257u // "WRM2" - v2: role + protected only; bit 6 was still timestamp. On upgrade
|
||||
// we clear the signer bit, then rewrite as v3.
|
||||
#define WARM_STORE_MAGIC_V1 \
|
||||
0x314D5257u // "WRM1" - v1: last_heard was a plain timestamp. On upgrade we keep
|
||||
// identity + key but discard last_heard, then rewrite as v2.
|
||||
// identity + key but discard last_heard, then rewrite as v3.
|
||||
|
||||
#ifdef FSCom
|
||||
static const char *warmFileName = "/prefs/warm.dat";
|
||||
@@ -134,18 +136,18 @@ WarmNodeEntry *WarmNodeStore::place(NodeNum num, uint32_t lastHeard, const uint8
|
||||
return slot;
|
||||
}
|
||||
|
||||
bool WarmNodeStore::absorb(NodeNum num, uint32_t lastHeard, const uint8_t *key32, uint8_t role, uint8_t protectedCat)
|
||||
bool WarmNodeStore::absorb(NodeNum num, uint32_t lastHeard, const uint8_t *key32, uint8_t role, uint8_t protectedCat, bool signer)
|
||||
{
|
||||
// Pack role + protected category into the low bits of last_heard. place() and ring
|
||||
// replay store the raw word verbatim, so the metadata round-trips through flash.
|
||||
const uint32_t packed = warmPackLastHeard(lastHeard, role, protectedCat);
|
||||
// Pack role + protected category + signer into the low bits of last_heard. place() and
|
||||
// ring replay store the raw word verbatim, so the metadata round-trips through flash.
|
||||
const uint32_t packed = warmPackLastHeard(lastHeard, role, protectedCat, signer);
|
||||
const WarmNodeEntry *slot = place(num, packed, key32);
|
||||
if (!slot)
|
||||
return false;
|
||||
persistEntry(*slot);
|
||||
LOG_MIGRATION("WarmStore absorb 0x%08x key=%d last_heard=%u role=%u prot=%u (now %u/%u)", (unsigned)num,
|
||||
LOG_MIGRATION("WarmStore absorb 0x%08x key=%d last_heard=%u role=%u prot=%u signer=%u (now %u/%u)", (unsigned)num,
|
||||
keyIsSet(slot->public_key) ? 1 : 0, (unsigned)warmTimeOf(*slot), (unsigned)role, (unsigned)protectedCat,
|
||||
(unsigned)count(), (unsigned)capacity());
|
||||
signer ? 1u : 0u, (unsigned)count(), (unsigned)capacity());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -258,19 +260,24 @@ bool WarmNodeStore::saveIfDirty()
|
||||
// (stranded live entries re-appended, then erased). Flash access holds spiLock -
|
||||
// the page cache is shared with InternalFS/LittleFS.
|
||||
|
||||
bool WarmNodeStore::ringReadHeader(uint8_t page, WarmPageHeader &h, bool *legacy) const
|
||||
bool WarmNodeStore::ringReadHeader(uint8_t page, WarmPageHeader &h, WarmFormat *fmt) const
|
||||
{
|
||||
flash_nrf5x_read(&h, WARM_FLASH_PAGE_ADDR(page), sizeof(h));
|
||||
if (h.seq == 0xFFFFFFFFu)
|
||||
return false; // erased page
|
||||
if (h.magic == WARM_RING_MAGIC) {
|
||||
if (legacy)
|
||||
*legacy = false;
|
||||
if (fmt)
|
||||
*fmt = WarmFormat::Current;
|
||||
return true;
|
||||
}
|
||||
if (h.magic == WARM_RING_MAGIC_V2) {
|
||||
if (fmt)
|
||||
*fmt = WarmFormat::V2; // replay it, but clear the signer bit (see WARM_RING_MAGIC_V2)
|
||||
return true;
|
||||
}
|
||||
if (h.magic == WARM_RING_MAGIC_V1) {
|
||||
if (legacy)
|
||||
*legacy = true; // v1 page: replay it, but discard last_heard (see WARM_RING_MAGIC_V1)
|
||||
if (fmt)
|
||||
*fmt = WarmFormat::V1; // replay it, but discard last_heard (see WARM_RING_MAGIC_V1)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -386,13 +393,13 @@ void WarmNodeStore::load()
|
||||
// Order valid pages by ascending seq so replay applies oldest first
|
||||
uint8_t order[WARM_FLASH_PAGES] = {};
|
||||
uint32_t seqs[WARM_FLASH_PAGES] = {};
|
||||
bool legacyOf[WARM_FLASH_PAGES] = {}; // per-page: v1 (WRNG) → discard last_heard on replay
|
||||
WarmFormat fmtOf[WARM_FLASH_PAGES] = {}; // per-page on-flash format; older ones are normalised on replay
|
||||
uint8_t nValid = 0;
|
||||
uint8_t nCorrupt = 0;
|
||||
for (uint8_t p = 0; p < WARM_FLASH_PAGES; p++) {
|
||||
WarmPageHeader h;
|
||||
bool legacy = false;
|
||||
if (!ringReadHeader(p, h, &legacy)) {
|
||||
WarmFormat fmt = WarmFormat::Current;
|
||||
if (!ringReadHeader(p, h, &fmt)) {
|
||||
// An erased page reads back all-ones; any other magic is a
|
||||
// partially-written or bit-rotted header we're dropping, so flag it
|
||||
// rather than silently treating the loss as a clean empty ring.
|
||||
@@ -400,7 +407,7 @@ void WarmNodeStore::load()
|
||||
nCorrupt++;
|
||||
continue;
|
||||
}
|
||||
legacyOf[p] = legacy;
|
||||
fmtOf[p] = fmt;
|
||||
uint8_t pos = nValid;
|
||||
while (pos > 0 && static_cast<int32_t>(h.seq - seqs[pos - 1]) < 0) {
|
||||
order[pos] = order[pos - 1];
|
||||
@@ -427,7 +434,7 @@ void WarmNodeStore::load()
|
||||
uint32_t migrated = 0;
|
||||
for (uint8_t k = 0; k < nValid; k++) {
|
||||
const uint8_t p = order[k];
|
||||
const bool legacy = legacyOf[p];
|
||||
const WarmFormat fmt = fmtOf[p];
|
||||
uint16_t slot = 0;
|
||||
for (; slot < kRecordsPerPage; slot++) {
|
||||
WarmNodeEntry rec;
|
||||
@@ -444,12 +451,15 @@ void WarmNodeStore::load()
|
||||
memset(e, 0, sizeof(*e));
|
||||
}
|
||||
} else {
|
||||
// v1 (legacy) record: keep identity + key, but discard the old timestamp -
|
||||
// its low bits would otherwise be misread as role/protected metadata.
|
||||
// Normalise older records: v1's timestamp would be misread as role/protected,
|
||||
// and v2's bit 6 as a signer we never verified.
|
||||
uint32_t lh = rec.last_heard;
|
||||
if (legacy) {
|
||||
if (fmt == WarmFormat::V1) {
|
||||
lh = 0;
|
||||
migrated++;
|
||||
} else if (fmt == WarmFormat::V2) {
|
||||
lh &= ~(WARM_SIGNER_MASK << WARM_SIGNER_SHIFT);
|
||||
migrated++;
|
||||
}
|
||||
const WarmNodeEntry *e = place(rec.num, lh, rec.public_key);
|
||||
if (e)
|
||||
@@ -460,17 +470,16 @@ void WarmNodeStore::load()
|
||||
activePage = p;
|
||||
writeSlot = slot;
|
||||
nextSeq = seqs[k] + 1;
|
||||
// If the head is a v1 page, force the next append to rotate into a fresh v2 page,
|
||||
// so new (v2) records never land in a page whose header says v1 (which would make
|
||||
// a later load discard their last_heard - including the role/protected we just set).
|
||||
if (legacy)
|
||||
// Rotate rather than append into an older-format page: a later load would
|
||||
// normalise the new records to that page's format and drop metadata.
|
||||
if (fmt != WarmFormat::Current)
|
||||
writeSlot = kRecordsPerPage;
|
||||
}
|
||||
}
|
||||
if (nCorrupt)
|
||||
LOG_WARN("WarmStore: dropped %u corrupt ring page(s), some nodes lost", nCorrupt);
|
||||
if (migrated)
|
||||
LOG_INFO("WarmStore: migrated %u v1 record(s) (kept key, discarded last_heard)", (unsigned)migrated);
|
||||
LOG_INFO("WarmStore: migrated %u legacy record(s) to the current format", (unsigned)migrated);
|
||||
LOG_INFO("WarmStore: replayed %u ring records -> %u live nodes (page %u, slot %u)", (unsigned)replayed, (unsigned)count(),
|
||||
activePage, writeSlot);
|
||||
}
|
||||
@@ -537,10 +546,14 @@ void WarmNodeStore::load()
|
||||
LOG_WARN("WarmStore: %s header read failed, starting empty", warmFileName);
|
||||
return;
|
||||
}
|
||||
// v1 (WRM1) is still accepted: same record size, but its last_heard was a plain
|
||||
// timestamp. We keep identity + key and discard last_heard on load (see below).
|
||||
const bool legacy = (h.magic == WARM_STORE_MAGIC_V1);
|
||||
if ((h.magic != WARM_STORE_MAGIC && !legacy) || h.entrySize != sizeof(WarmNodeEntry) || h.count > WARM_NODE_COUNT) {
|
||||
// Older snapshots are still accepted: same record size, fewer metadata bits in
|
||||
// last_heard. Both are normalised to the current format below.
|
||||
const bool known = h.magic == WARM_STORE_MAGIC || h.magic == WARM_STORE_MAGIC_V2 || h.magic == WARM_STORE_MAGIC_V1;
|
||||
const WarmFormat fmt = h.magic == WARM_STORE_MAGIC_V1 ? WarmFormat::V1
|
||||
: h.magic == WARM_STORE_MAGIC_V2 ? WarmFormat::V2
|
||||
: WarmFormat::Current;
|
||||
const bool legacy = fmt != WarmFormat::Current;
|
||||
if (!known || h.entrySize != sizeof(WarmNodeEntry) || h.count > WARM_NODE_COUNT) {
|
||||
f.close();
|
||||
LOG_WARN("WarmStore: %s header invalid (magic=0x%08x entrySize=%u count=%u), starting empty", warmFileName, h.magic,
|
||||
h.entrySize, h.count);
|
||||
@@ -560,19 +573,24 @@ void WarmNodeStore::load()
|
||||
memset(entries, 0, WARM_NODE_COUNT * sizeof(WarmNodeEntry));
|
||||
return;
|
||||
}
|
||||
if (legacy) {
|
||||
// Migrate v1 → v2: discard the old last_heard (its low bits would be misread as
|
||||
// role/protected); keep num + public_key. Mark dirty so save() rewrites as v2.
|
||||
// Normalise older records, then mark dirty so save() rewrites in the current format:
|
||||
// v1's timestamp would be misread as role/protected, v2's bit 6 as an unverified signer.
|
||||
if (fmt == WarmFormat::V1) {
|
||||
for (size_t i = 0; i < WARM_NODE_COUNT; i++)
|
||||
if (entries[i].num)
|
||||
entries[i].last_heard = 0;
|
||||
dirty = true;
|
||||
} else if (fmt == WarmFormat::V2) {
|
||||
for (size_t i = 0; i < WARM_NODE_COUNT; i++)
|
||||
if (entries[i].num)
|
||||
entries[i].last_heard &= ~(WARM_SIGNER_MASK << WARM_SIGNER_SHIFT);
|
||||
dirty = true;
|
||||
}
|
||||
} else {
|
||||
f.close();
|
||||
}
|
||||
LOG_INFO("WarmStore: loaded %u warm nodes from %s%s", h.count, warmFileName,
|
||||
legacy ? " (v1 migrated: discarded last_heard)" : "");
|
||||
legacy ? " (migrated from an older format)" : "");
|
||||
}
|
||||
|
||||
bool WarmNodeStore::save()
|
||||
|
||||
+22
-10
@@ -43,27 +43,33 @@ static_assert(sizeof(WarmNodeEntry) == 40, "WarmNodeEntry must stay 40 B - persi
|
||||
//
|
||||
// The warm tier only uses last_heard to LRU-rank evicted (long-tail) nodes, so ~minute
|
||||
// recency resolution is plenty. We reclaim the low WARM_META_BITS of that field to carry
|
||||
// the evicted node's device role + a protected category, at zero cost to record size
|
||||
// (entry stays 40 B; no RAM/flash growth). The high bits remain a real unix-seconds
|
||||
// timestamp quantised to (1 << WARM_META_BITS) seconds.
|
||||
// the evicted node's device role, a protected category + a signer flag, at zero cost to
|
||||
// record size (entry stays 40 B; no RAM/flash growth). The high bits remain a real
|
||||
// unix-seconds timestamp quantised to (1 << WARM_META_BITS) seconds.
|
||||
//
|
||||
// Safe because: a real timestamp can never be all-ones (the tombstone sentinel) before
|
||||
// 2106, and tombstones/erased flash are detected via num before last_heard is read. Only
|
||||
// the LOW bits are stolen - the high (era) bits are untouched, so the time range is intact.
|
||||
static constexpr uint32_t WARM_META_BITS = 6; // role(4) + protected(2)
|
||||
static constexpr uint32_t WARM_META_MASK = (1u << WARM_META_BITS) - 1; // 0x3F → 64 s quantum
|
||||
static constexpr uint32_t WARM_TIME_MASK = ~WARM_META_MASK; // 0xFFFFFFC0
|
||||
static constexpr uint32_t WARM_META_BITS = 7; // role(4) + protected(2) + signer(1)
|
||||
static constexpr uint32_t WARM_META_MASK = (1u << WARM_META_BITS) - 1; // 0x7F → 128 s quantum
|
||||
static constexpr uint32_t WARM_TIME_MASK = ~WARM_META_MASK; // 0xFFFFFF80
|
||||
static constexpr uint32_t WARM_ROLE_MASK = 0x0Fu; // bits [3:0] device role (0..12)
|
||||
static constexpr uint32_t WARM_PROT_SHIFT = 4; // bits [5:4] protected category
|
||||
static constexpr uint32_t WARM_PROT_MASK = 0x03u;
|
||||
static constexpr uint32_t WARM_SIGNER_SHIFT = 6; // bit [6] we verified an XEdDSA signature from this node
|
||||
static constexpr uint32_t WARM_SIGNER_MASK = 0x01u;
|
||||
|
||||
// On-disk record format, from the page/file magic; older ones are normalised by load().
|
||||
enum class WarmFormat : uint8_t { Current, V2, V1 };
|
||||
|
||||
// Protected category cached alongside role so consumers needn't re-derive the mapping.
|
||||
enum class WarmProtected : uint8_t { None = 0, Role = 1, Flag = 2, XeddsaSigner = 3 };
|
||||
|
||||
inline uint32_t warmPackLastHeard(uint32_t lastHeard, uint8_t role, uint8_t prot)
|
||||
inline uint32_t warmPackLastHeard(uint32_t lastHeard, uint8_t role, uint8_t prot, bool signer)
|
||||
{
|
||||
return (lastHeard & WARM_TIME_MASK) | (static_cast<uint32_t>(role) & WARM_ROLE_MASK) |
|
||||
((static_cast<uint32_t>(prot) & WARM_PROT_MASK) << WARM_PROT_SHIFT);
|
||||
((static_cast<uint32_t>(prot) & WARM_PROT_MASK) << WARM_PROT_SHIFT) |
|
||||
((signer ? WARM_SIGNER_MASK : 0u) << WARM_SIGNER_SHIFT);
|
||||
}
|
||||
inline uint32_t warmTimeOf(const WarmNodeEntry &e)
|
||||
{
|
||||
@@ -77,6 +83,10 @@ inline uint8_t warmProtOf(const WarmNodeEntry &e)
|
||||
{
|
||||
return static_cast<uint8_t>((e.last_heard >> WARM_PROT_SHIFT) & WARM_PROT_MASK);
|
||||
}
|
||||
inline bool warmSignerOf(const WarmNodeEntry &e)
|
||||
{
|
||||
return ((e.last_heard >> WARM_SIGNER_SHIFT) & WARM_SIGNER_MASK) != 0;
|
||||
}
|
||||
|
||||
// Gated on NRF52840_XXAA: the ring sits at 0xEA000
|
||||
// valid only on the 1 MB-flash nRF52840.
|
||||
@@ -99,9 +109,11 @@ class WarmNodeStore
|
||||
/// entries; otherwise the oldest (keyless-first) entry is replaced.
|
||||
/// @param role the node's device role (meshtastic_Config_DeviceConfig_Role, 0..12)
|
||||
/// @param protectedCat WarmProtected category cached for the hop-trim path
|
||||
/// @param signer true if we ever verified an XEdDSA signature from this node, so
|
||||
/// re-admission restores the bit rather than relearning it
|
||||
/// @return true if the node was stored or updated
|
||||
bool absorb(NodeNum num, uint32_t lastHeard, const uint8_t *key32 /* may be NULL */, uint8_t role = 0,
|
||||
uint8_t protectedCat = 0);
|
||||
uint8_t protectedCat = 0, bool signer = false);
|
||||
|
||||
/// Look up the cached device role + protected category for a warm node.
|
||||
/// @return false if the node is not in the warm tier.
|
||||
@@ -167,7 +179,7 @@ class WarmNodeStore
|
||||
void ringAppend(const WarmNodeEntry &rec, int storeSlot /* -1 for tombstones */);
|
||||
void ringRotate(); // reclaim oldest page, compacting stranded live entries
|
||||
void ringOpenPage(uint8_t page); // erase + write header (seq = nextSeq++)
|
||||
bool ringReadHeader(uint8_t page, WarmPageHeader &h, bool *legacy = nullptr) const;
|
||||
bool ringReadHeader(uint8_t page, WarmPageHeader &h, WarmFormat *fmt = nullptr) const;
|
||||
#endif
|
||||
|
||||
bool save();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#include "mesh/eth/ethClient.h"
|
||||
#include "NodeDB.h"
|
||||
#include "RTC.h"
|
||||
#include "concurrency/Periodic.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "main.h"
|
||||
#include "mesh/api/ethServerAPI.h"
|
||||
#include "target_specific.h"
|
||||
|
||||
@@ -69,6 +69,8 @@ PB_BIND(meshtastic_Config_SessionkeyConfig, meshtastic_Config_SessionkeyConfig,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -374,7 +374,11 @@ typedef enum _meshtastic_Config_LoRaConfig_ModemPreset {
|
||||
Note: TCXO with tight tolerances (±5 ppm or better) is *absolutely required* at these narrow bandwidths.
|
||||
Only compatible with SX127x and SX126x chipsets.
|
||||
Comparable link budget and data rate to LONG_MODERATE. */
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_TINY_SLOW = 15
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_TINY_SLOW = 15,
|
||||
/* Medium Range - Turbo
|
||||
This preset performs similarly to MEDIUM_FAST, but with 500kHz bandwidth.
|
||||
It is not legal to use in all regions due to this wider bandwidth. */
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_TURBO = 16
|
||||
} meshtastic_Config_LoRaConfig_ModemPreset;
|
||||
|
||||
typedef enum _meshtastic_Config_LoRaConfig_FEM_LNA_Mode {
|
||||
@@ -395,6 +399,19 @@ typedef enum _meshtastic_Config_BluetoothConfig_PairingMode {
|
||||
meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN = 2
|
||||
} meshtastic_Config_BluetoothConfig_PairingMode;
|
||||
|
||||
/* Controls how the device authenticates remotely received mesh packets. */
|
||||
typedef enum _meshtastic_Config_SecurityConfig_PacketSignaturePolicy {
|
||||
/* Accept unsigned packets for maximum compatibility while still rejecting malformed or invalid signatures.
|
||||
This is the default to avoid legacy nodes dropping signed packets during rebroadcast. */
|
||||
meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_COMPATIBLE = 0,
|
||||
/* Prefer authenticated packets while retaining compatibility with unsigned packets from nodes not known to sign.
|
||||
Rejects unsigned, signable broadcasts from nodes that have previously signed. */
|
||||
meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_BALANCED = 1,
|
||||
/* Accept only packets authenticated by a verified XEdDSA signature or successful PKI decryption.
|
||||
Unsigned, malformed, invalid, or unverifiable packets are ignored. */
|
||||
meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_STRICT = 2
|
||||
} meshtastic_Config_SecurityConfig_PacketSignaturePolicy;
|
||||
|
||||
/* Struct definitions */
|
||||
/* Configuration */
|
||||
typedef struct _meshtastic_Config_DeviceConfig {
|
||||
@@ -685,6 +702,8 @@ typedef struct _meshtastic_Config_SecurityConfig {
|
||||
bool debug_log_api_enabled;
|
||||
/* Allow incoming device control over the insecure legacy admin channel. */
|
||||
bool admin_channel_enabled;
|
||||
/* Determines the packet signature policy applied to remotely received mesh packets. */
|
||||
meshtastic_Config_SecurityConfig_PacketSignaturePolicy packet_signature_policy;
|
||||
} meshtastic_Config_SecurityConfig;
|
||||
|
||||
/* Blank config request, strictly for getting the session key */
|
||||
@@ -767,8 +786,8 @@ extern "C" {
|
||||
#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_ITU2_125CM+1))
|
||||
|
||||
#define _meshtastic_Config_LoRaConfig_ModemPreset_MIN meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST
|
||||
#define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_TINY_SLOW
|
||||
#define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_TINY_SLOW+1))
|
||||
#define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_TURBO
|
||||
#define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_TURBO+1))
|
||||
|
||||
#define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED
|
||||
#define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MAX meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT
|
||||
@@ -778,6 +797,10 @@ extern "C" {
|
||||
#define _meshtastic_Config_BluetoothConfig_PairingMode_MAX meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN
|
||||
#define _meshtastic_Config_BluetoothConfig_PairingMode_ARRAYSIZE ((meshtastic_Config_BluetoothConfig_PairingMode)(meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN+1))
|
||||
|
||||
#define _meshtastic_Config_SecurityConfig_PacketSignaturePolicy_MIN meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_COMPATIBLE
|
||||
#define _meshtastic_Config_SecurityConfig_PacketSignaturePolicy_MAX meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_STRICT
|
||||
#define _meshtastic_Config_SecurityConfig_PacketSignaturePolicy_ARRAYSIZE ((meshtastic_Config_SecurityConfig_PacketSignaturePolicy)(meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_STRICT+1))
|
||||
|
||||
|
||||
#define meshtastic_Config_DeviceConfig_role_ENUMTYPE meshtastic_Config_DeviceConfig_Role
|
||||
#define meshtastic_Config_DeviceConfig_rebroadcast_mode_ENUMTYPE meshtastic_Config_DeviceConfig_RebroadcastMode
|
||||
@@ -801,6 +824,7 @@ extern "C" {
|
||||
|
||||
#define meshtastic_Config_BluetoothConfig_mode_ENUMTYPE meshtastic_Config_BluetoothConfig_PairingMode
|
||||
|
||||
#define meshtastic_Config_SecurityConfig_packet_signature_policy_ENUMTYPE meshtastic_Config_SecurityConfig_PacketSignaturePolicy
|
||||
|
||||
|
||||
|
||||
@@ -814,7 +838,7 @@ extern "C" {
|
||||
#define meshtastic_Config_DisplayConfig_init_default {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0, 0}
|
||||
#define meshtastic_Config_LoRaConfig_init_default {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0, _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN, 0}
|
||||
#define meshtastic_Config_BluetoothConfig_init_default {0, _meshtastic_Config_BluetoothConfig_PairingMode_MIN, 0}
|
||||
#define meshtastic_Config_SecurityConfig_init_default {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0}
|
||||
#define meshtastic_Config_SecurityConfig_init_default {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0, _meshtastic_Config_SecurityConfig_PacketSignaturePolicy_MIN}
|
||||
#define meshtastic_Config_SessionkeyConfig_init_default {0}
|
||||
#define meshtastic_Config_init_zero {0, {meshtastic_Config_DeviceConfig_init_zero}}
|
||||
#define meshtastic_Config_DeviceConfig_init_zero {_meshtastic_Config_DeviceConfig_Role_MIN, 0, 0, 0, _meshtastic_Config_DeviceConfig_RebroadcastMode_MIN, 0, 0, 0, 0, "", 0, _meshtastic_Config_DeviceConfig_BuzzerMode_MIN}
|
||||
@@ -825,7 +849,7 @@ extern "C" {
|
||||
#define meshtastic_Config_DisplayConfig_init_zero {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0, 0}
|
||||
#define meshtastic_Config_LoRaConfig_init_zero {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0, _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN, 0}
|
||||
#define meshtastic_Config_BluetoothConfig_init_zero {0, _meshtastic_Config_BluetoothConfig_PairingMode_MIN, 0}
|
||||
#define meshtastic_Config_SecurityConfig_init_zero {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0}
|
||||
#define meshtastic_Config_SecurityConfig_init_zero {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0, _meshtastic_Config_SecurityConfig_PacketSignaturePolicy_MIN}
|
||||
#define meshtastic_Config_SessionkeyConfig_init_zero {0}
|
||||
|
||||
/* Field tags (for use in manual encoding/decoding) */
|
||||
@@ -921,6 +945,7 @@ extern "C" {
|
||||
#define meshtastic_Config_SecurityConfig_serial_enabled_tag 5
|
||||
#define meshtastic_Config_SecurityConfig_debug_log_api_enabled_tag 6
|
||||
#define meshtastic_Config_SecurityConfig_admin_channel_enabled_tag 8
|
||||
#define meshtastic_Config_SecurityConfig_packet_signature_policy_tag 9
|
||||
#define meshtastic_Config_device_tag 1
|
||||
#define meshtastic_Config_position_tag 2
|
||||
#define meshtastic_Config_power_tag 3
|
||||
@@ -1082,7 +1107,8 @@ X(a, STATIC, REPEATED, BYTES, admin_key, 3) \
|
||||
X(a, STATIC, SINGULAR, BOOL, is_managed, 4) \
|
||||
X(a, STATIC, SINGULAR, BOOL, serial_enabled, 5) \
|
||||
X(a, STATIC, SINGULAR, BOOL, debug_log_api_enabled, 6) \
|
||||
X(a, STATIC, SINGULAR, BOOL, admin_channel_enabled, 8)
|
||||
X(a, STATIC, SINGULAR, BOOL, admin_channel_enabled, 8) \
|
||||
X(a, STATIC, SINGULAR, UENUM, packet_signature_policy, 9)
|
||||
#define meshtastic_Config_SecurityConfig_CALLBACK NULL
|
||||
#define meshtastic_Config_SecurityConfig_DEFAULT NULL
|
||||
|
||||
@@ -1126,7 +1152,7 @@ extern const pb_msgdesc_t meshtastic_Config_SessionkeyConfig_msg;
|
||||
#define meshtastic_Config_NetworkConfig_size 204
|
||||
#define meshtastic_Config_PositionConfig_size 62
|
||||
#define meshtastic_Config_PowerConfig_size 52
|
||||
#define meshtastic_Config_SecurityConfig_size 178
|
||||
#define meshtastic_Config_SecurityConfig_size 180
|
||||
#define meshtastic_Config_SessionkeyConfig_size 0
|
||||
#define meshtastic_Config_size 207
|
||||
|
||||
|
||||
@@ -452,7 +452,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg;
|
||||
/* Maximum encoded size of messages (where known) */
|
||||
/* meshtastic_NodeDatabase_size depends on runtime parameters */
|
||||
#define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size
|
||||
#define meshtastic_BackupPreferences_size 2738
|
||||
#define meshtastic_BackupPreferences_size 2740
|
||||
#define meshtastic_ChannelFile_size 718
|
||||
#define meshtastic_DeviceState_size 1944
|
||||
#define meshtastic_NodeEnvironmentEntry_size 170
|
||||
|
||||
@@ -6,10 +6,34 @@
|
||||
#error Regenerate this file with the current version of nanopb generator.
|
||||
#endif
|
||||
|
||||
PB_BIND(meshtastic_SensorData, meshtastic_SensorData, AUTO)
|
||||
PB_BIND(meshtastic_FileTransfer, meshtastic_FileTransfer, 4)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_DirectoryListing, meshtastic_DirectoryListing, 4)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_I2CTransaction, meshtastic_I2CTransaction, 2)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_SdCardInfo, meshtastic_SdCardInfo, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_I2CResult, meshtastic_I2CResult, 2)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_InterdeviceMessage, meshtastic_InterdeviceMessage, 4)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
PB_BIND(meshtastic_InterdeviceMessage, meshtastic_InterdeviceMessage, 2)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -10,38 +10,194 @@
|
||||
#endif
|
||||
|
||||
/* Enum definitions */
|
||||
typedef enum _meshtastic_MessageType {
|
||||
meshtastic_MessageType_ACK = 0,
|
||||
meshtastic_MessageType_COLLECT_INTERVAL = 160, /* in ms */
|
||||
meshtastic_MessageType_BEEP_ON = 161, /* duration ms */
|
||||
meshtastic_MessageType_BEEP_OFF = 162, /* cancel prematurely */
|
||||
meshtastic_MessageType_SHUTDOWN = 163,
|
||||
meshtastic_MessageType_POWER_ON = 164,
|
||||
meshtastic_MessageType_SCD41_TEMP = 176,
|
||||
meshtastic_MessageType_SCD41_HUMIDITY = 177,
|
||||
meshtastic_MessageType_SCD41_CO2 = 178,
|
||||
meshtastic_MessageType_AHT20_TEMP = 179,
|
||||
meshtastic_MessageType_AHT20_HUMIDITY = 180,
|
||||
meshtastic_MessageType_TVOC_INDEX = 181
|
||||
} meshtastic_MessageType;
|
||||
/* Version of the interdevice protocol spoken on the link. Both sides send
|
||||
theirs in the ping/pong handshake; a peer reporting a different one runs
|
||||
firmware that does not match and is not talked to.
|
||||
|
||||
On a change that breaks the other side (renumbered fields, changed
|
||||
semantics, removed messages), raise the value of CURRENT. Do not add
|
||||
another entry: this enum carries a single constant, not a history. */
|
||||
typedef enum _meshtastic_InterdeviceVersion {
|
||||
meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_UNSPECIFIED = 0,
|
||||
/* Never use 1: ping/pong were bools before the handshake existed, and a
|
||||
bool true is the same varint on the wire as the number 1, so firmware
|
||||
predating the handshake would pass it. */
|
||||
meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_CURRENT = 2
|
||||
} meshtastic_InterdeviceVersion;
|
||||
|
||||
/* Defines the supported file operations */
|
||||
typedef enum _meshtastic_FileOperation {
|
||||
meshtastic_FileOperation_GET = 0,
|
||||
meshtastic_FileOperation_POST = 1,
|
||||
meshtastic_FileOperation_PUT = 2,
|
||||
meshtastic_FileOperation_DELETE = 3
|
||||
} meshtastic_FileOperation;
|
||||
|
||||
/* Outcome of a file or directory operation. The requester must be able to
|
||||
tell a transient condition from a definitive one: BUSY is worth another
|
||||
try, NOT_FOUND is not. */
|
||||
typedef enum _meshtastic_FileStatus {
|
||||
meshtastic_FileStatus_FILE_UNSPECIFIED = 0,
|
||||
meshtastic_FileStatus_FILE_OK = 1,
|
||||
/* Retry later: the co-processor is doing card maintenance (mount,
|
||||
free space scan) and cannot serve the request right now */
|
||||
meshtastic_FileStatus_FILE_BUSY = 2,
|
||||
meshtastic_FileStatus_FILE_NO_CARD = 3,
|
||||
meshtastic_FileStatus_FILE_NOT_FOUND = 4,
|
||||
/* PUT only: offset did not match the current end of the file. file_size
|
||||
carries the size the file actually has, so the writer can resync (or
|
||||
recognize its own chunk as already written after a lost response). */
|
||||
meshtastic_FileStatus_FILE_OFFSET_CONFLICT = 5,
|
||||
meshtastic_FileStatus_FILE_IO_ERROR = 6,
|
||||
meshtastic_FileStatus_FILE_NOT_A_FILE = 7 /* path is a directory (GET) or not one (listing) */
|
||||
} meshtastic_FileStatus;
|
||||
|
||||
/* What to do with the SD card of the co-processor */
|
||||
typedef enum _meshtastic_SdCommand {
|
||||
meshtastic_SdCommand_SD_COMMAND_UNSPECIFIED = 0,
|
||||
meshtastic_SdCommand_SD_MOUNT = 1, /* mount a card that is in the slot, also after an eject */
|
||||
meshtastic_SdCommand_SD_EJECT = 2, /* flush and release the card so it can be pulled safely */
|
||||
meshtastic_SdCommand_SD_FORMAT = 3 /* wipe the card and put a fresh FAT on it, then mount it */
|
||||
} meshtastic_SdCommand;
|
||||
|
||||
typedef enum _meshtastic_SdCardInfo_CardType {
|
||||
meshtastic_SdCardInfo_CardType_NONE = 0,
|
||||
meshtastic_SdCardInfo_CardType_MMC = 1,
|
||||
meshtastic_SdCardInfo_CardType_SD = 2,
|
||||
meshtastic_SdCardInfo_CardType_SDHC = 3,
|
||||
meshtastic_SdCardInfo_CardType_SDXC = 4,
|
||||
meshtastic_SdCardInfo_CardType_UNKNOWN_CARD = 5
|
||||
} meshtastic_SdCardInfo_CardType;
|
||||
|
||||
typedef enum _meshtastic_SdCardInfo_FatType {
|
||||
meshtastic_SdCardInfo_FatType_UNKNOWN_FAT = 0,
|
||||
meshtastic_SdCardInfo_FatType_FAT16 = 1,
|
||||
meshtastic_SdCardInfo_FatType_FAT32 = 2,
|
||||
meshtastic_SdCardInfo_FatType_EXFAT = 3
|
||||
} meshtastic_SdCardInfo_FatType;
|
||||
|
||||
typedef enum _meshtastic_I2CResult_Status {
|
||||
/* Never sent: an all-defaults (e.g. accidentally empty) message must
|
||||
not decode as a successful transaction */
|
||||
meshtastic_I2CResult_Status_UNSPECIFIED = 0,
|
||||
meshtastic_I2CResult_Status_OK = 1,
|
||||
meshtastic_I2CResult_Status_NACK_ADDRESS = 2,
|
||||
meshtastic_I2CResult_Status_NACK_DATA = 3,
|
||||
meshtastic_I2CResult_Status_ERROR = 4
|
||||
} meshtastic_I2CResult_Status;
|
||||
|
||||
/* Struct definitions */
|
||||
typedef struct _meshtastic_SensorData {
|
||||
/* The message type */
|
||||
meshtastic_MessageType type;
|
||||
pb_size_t which_data;
|
||||
union {
|
||||
float float_value;
|
||||
uint32_t uint32_value;
|
||||
} data;
|
||||
} meshtastic_SensorData;
|
||||
typedef PB_BYTES_ARRAY_T(4096) meshtastic_FileTransfer_filedata_t;
|
||||
/* Message for file operations */
|
||||
typedef struct _meshtastic_FileTransfer {
|
||||
meshtastic_FileOperation operation; /* File operation (GET, POST, PUT, DELETE) */
|
||||
char filepath[256]; /* Path of the file on the SD card */
|
||||
meshtastic_FileTransfer_filedata_t filedata; /* Chunk content (POST/PUT request, GET response) */
|
||||
meshtastic_FileStatus status; /* Response: outcome of the operation */
|
||||
char message[255]; /* Response: human readable detail, may be empty */
|
||||
uint64_t offset; /* Byte offset of this chunk within the file (ranged GET/PUT) */
|
||||
/* GET request: number of bytes to read, 0 = max chunk size. A response
|
||||
carries at most the filedata max_size (see interdevice.options) per
|
||||
chunk; larger requests are truncated, visible in the filedata length. */
|
||||
uint32_t length;
|
||||
uint64_t file_size; /* GET response: total size of the file */
|
||||
} meshtastic_FileTransfer;
|
||||
|
||||
/* Message for structured directory listing */
|
||||
typedef struct _meshtastic_DirectoryListing {
|
||||
char directory[256]; /* Path of the directory */
|
||||
/* One page of entry names, full FAT LFN length. Subdirectories carry a
|
||||
trailing slash. Note that a name whose directory prefix pushes the
|
||||
combined path past the FileTransfer.filepath limit cannot round-trip.
|
||||
Page size is the max_count in interdevice.options; page through with
|
||||
offset and total_count. */
|
||||
pb_size_t filenames_count;
|
||||
char filenames[16][256];
|
||||
meshtastic_FileStatus status; /* Response: outcome of the operation */
|
||||
char message[255]; /* Response: human readable detail, may be empty */
|
||||
uint32_t offset; /* Request: skip this many entries (paging) */
|
||||
uint32_t total_count; /* Response: total number of entries in the directory */
|
||||
} meshtastic_DirectoryListing;
|
||||
|
||||
typedef PB_BYTES_ARRAY_T(256) meshtastic_I2CTransaction_write_data_t;
|
||||
/* A single I2C transaction: an optional write followed by an optional
|
||||
read with repeated start, matching the TwoWire usage of sensor drivers
|
||||
(beginTransmission/write.../endTransmission(false)/requestFrom) */
|
||||
typedef struct _meshtastic_I2CTransaction {
|
||||
uint32_t address; /* 7-bit device address */
|
||||
meshtastic_I2CTransaction_write_data_t write_data; /* Bytes to write, may be empty */
|
||||
/* Number of bytes to read after the write, 0 = write-only. Bounded by
|
||||
the read_data max_size of I2CResult (see interdevice.options); larger
|
||||
requests are truncated, visible in the returned byte count. */
|
||||
uint32_t read_len;
|
||||
} meshtastic_I2CTransaction;
|
||||
|
||||
/* SD card statistics */
|
||||
typedef struct _meshtastic_SdCardInfo {
|
||||
/* Card initialized and usable. False while `busy` is set does not mean
|
||||
there is no card: the co-processor does not know yet. */
|
||||
bool present;
|
||||
meshtastic_SdCardInfo_CardType card_type;
|
||||
meshtastic_SdCardInfo_FatType fat_type;
|
||||
uint64_t card_size; /* Filesystem size in bytes */
|
||||
uint64_t used_bytes; /* Used bytes (may be expensive to compute on FAT32) */
|
||||
uint64_t free_bytes; /* Free bytes */
|
||||
/* used_bytes/free_bytes are only meaningful when true: the scan behind
|
||||
them runs in the background after mount and can take a while, and a
|
||||
full card is otherwise indistinguishable from a scan in progress */
|
||||
bool stats_valid;
|
||||
/* The co-processor is mounting a card right now, so whether one is
|
||||
present is not decided yet. Ask again rather than concluding the slot
|
||||
is empty. */
|
||||
bool busy;
|
||||
/* A card answers in the slot but carries no filesystem that could be
|
||||
mounted (present is false then). Formatting it makes it usable. */
|
||||
bool unformatted;
|
||||
} meshtastic_SdCardInfo;
|
||||
|
||||
typedef PB_BYTES_ARRAY_T(256) meshtastic_I2CResult_read_data_t;
|
||||
/* Result of an I2CTransaction */
|
||||
typedef struct _meshtastic_I2CResult {
|
||||
meshtastic_I2CResult_Status status;
|
||||
meshtastic_I2CResult_read_data_t read_data; /* Data read from the device, empty for write-only transactions */
|
||||
} meshtastic_I2CResult;
|
||||
|
||||
typedef PB_BYTES_ARRAY_T(128) meshtastic_InterdeviceMessage_i2c_scan_result_t;
|
||||
/* Main message for interdevice communication */
|
||||
typedef struct _meshtastic_InterdeviceMessage {
|
||||
pb_size_t which_data;
|
||||
union {
|
||||
char nmea[1024];
|
||||
meshtastic_SensorData sensor;
|
||||
uint16_t beep;
|
||||
meshtastic_I2CTransaction i2c_transaction;
|
||||
meshtastic_I2CResult i2c_result;
|
||||
bool i2c_scan; /* Request: scan the secondary I2C bus */
|
||||
meshtastic_InterdeviceMessage_i2c_scan_result_t i2c_scan_result; /* Response: 7-bit addresses of discovered devices */
|
||||
meshtastic_FileTransfer file_transfer;
|
||||
meshtastic_DirectoryListing directory_listing;
|
||||
bool get_sd_info; /* Request: SD card statistics */
|
||||
meshtastic_SdCardInfo sd_info; /* Response */
|
||||
/* Link liveness probe and version handshake. The receiver answers ping
|
||||
with pong, echoing the id. Touches no peripherals, so it works with
|
||||
nothing attached. Both carry the version the sender speaks; a peer
|
||||
that answers with a different one speaks another protocol and must
|
||||
not be used. */
|
||||
meshtastic_InterdeviceVersion ping;
|
||||
meshtastic_InterdeviceVersion pong;
|
||||
/* Response: the request could not be decoded or is of an unhandled
|
||||
type, so the requester fails fast instead of burning its timeout.
|
||||
Echoes the id when known, 0 when the frame was undecodable. Never
|
||||
sent in reaction to a nack. */
|
||||
bool nack;
|
||||
/* Request: mount the card, or release it so it can be pulled safely. The
|
||||
co-processor answers with sd_info. Without an eject the card is mounted
|
||||
on its own and kept mounted; after one it stays released until a mount
|
||||
is asked for. */
|
||||
meshtastic_SdCommand sd_command;
|
||||
} data;
|
||||
/* Correlates a response with its request: responses echo the id of the
|
||||
request they answer. 0 for unsolicited messages (e.g. the nmea stream). */
|
||||
uint32_t id;
|
||||
} meshtastic_InterdeviceMessage;
|
||||
|
||||
|
||||
@@ -50,53 +206,205 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
/* Helper constants for enums */
|
||||
#define _meshtastic_MessageType_MIN meshtastic_MessageType_ACK
|
||||
#define _meshtastic_MessageType_MAX meshtastic_MessageType_TVOC_INDEX
|
||||
#define _meshtastic_MessageType_ARRAYSIZE ((meshtastic_MessageType)(meshtastic_MessageType_TVOC_INDEX+1))
|
||||
#define _meshtastic_InterdeviceVersion_MIN meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_UNSPECIFIED
|
||||
#define _meshtastic_InterdeviceVersion_MAX meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_CURRENT
|
||||
#define _meshtastic_InterdeviceVersion_ARRAYSIZE ((meshtastic_InterdeviceVersion)(meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_CURRENT+1))
|
||||
|
||||
#define meshtastic_SensorData_type_ENUMTYPE meshtastic_MessageType
|
||||
#define _meshtastic_FileOperation_MIN meshtastic_FileOperation_GET
|
||||
#define _meshtastic_FileOperation_MAX meshtastic_FileOperation_DELETE
|
||||
#define _meshtastic_FileOperation_ARRAYSIZE ((meshtastic_FileOperation)(meshtastic_FileOperation_DELETE+1))
|
||||
|
||||
#define _meshtastic_FileStatus_MIN meshtastic_FileStatus_FILE_UNSPECIFIED
|
||||
#define _meshtastic_FileStatus_MAX meshtastic_FileStatus_FILE_NOT_A_FILE
|
||||
#define _meshtastic_FileStatus_ARRAYSIZE ((meshtastic_FileStatus)(meshtastic_FileStatus_FILE_NOT_A_FILE+1))
|
||||
|
||||
#define _meshtastic_SdCommand_MIN meshtastic_SdCommand_SD_COMMAND_UNSPECIFIED
|
||||
#define _meshtastic_SdCommand_MAX meshtastic_SdCommand_SD_FORMAT
|
||||
#define _meshtastic_SdCommand_ARRAYSIZE ((meshtastic_SdCommand)(meshtastic_SdCommand_SD_FORMAT+1))
|
||||
|
||||
#define _meshtastic_SdCardInfo_CardType_MIN meshtastic_SdCardInfo_CardType_NONE
|
||||
#define _meshtastic_SdCardInfo_CardType_MAX meshtastic_SdCardInfo_CardType_UNKNOWN_CARD
|
||||
#define _meshtastic_SdCardInfo_CardType_ARRAYSIZE ((meshtastic_SdCardInfo_CardType)(meshtastic_SdCardInfo_CardType_UNKNOWN_CARD+1))
|
||||
|
||||
#define _meshtastic_SdCardInfo_FatType_MIN meshtastic_SdCardInfo_FatType_UNKNOWN_FAT
|
||||
#define _meshtastic_SdCardInfo_FatType_MAX meshtastic_SdCardInfo_FatType_EXFAT
|
||||
#define _meshtastic_SdCardInfo_FatType_ARRAYSIZE ((meshtastic_SdCardInfo_FatType)(meshtastic_SdCardInfo_FatType_EXFAT+1))
|
||||
|
||||
#define _meshtastic_I2CResult_Status_MIN meshtastic_I2CResult_Status_UNSPECIFIED
|
||||
#define _meshtastic_I2CResult_Status_MAX meshtastic_I2CResult_Status_ERROR
|
||||
#define _meshtastic_I2CResult_Status_ARRAYSIZE ((meshtastic_I2CResult_Status)(meshtastic_I2CResult_Status_ERROR+1))
|
||||
|
||||
#define meshtastic_FileTransfer_operation_ENUMTYPE meshtastic_FileOperation
|
||||
#define meshtastic_FileTransfer_status_ENUMTYPE meshtastic_FileStatus
|
||||
|
||||
#define meshtastic_DirectoryListing_status_ENUMTYPE meshtastic_FileStatus
|
||||
|
||||
|
||||
#define meshtastic_SdCardInfo_card_type_ENUMTYPE meshtastic_SdCardInfo_CardType
|
||||
#define meshtastic_SdCardInfo_fat_type_ENUMTYPE meshtastic_SdCardInfo_FatType
|
||||
|
||||
#define meshtastic_I2CResult_status_ENUMTYPE meshtastic_I2CResult_Status
|
||||
|
||||
#define meshtastic_InterdeviceMessage_data_ping_ENUMTYPE meshtastic_InterdeviceVersion
|
||||
#define meshtastic_InterdeviceMessage_data_pong_ENUMTYPE meshtastic_InterdeviceVersion
|
||||
#define meshtastic_InterdeviceMessage_data_sd_command_ENUMTYPE meshtastic_SdCommand
|
||||
|
||||
|
||||
/* Initializer values for message structs */
|
||||
#define meshtastic_SensorData_init_default {_meshtastic_MessageType_MIN, 0, {0}}
|
||||
#define meshtastic_InterdeviceMessage_init_default {0, {""}}
|
||||
#define meshtastic_SensorData_init_zero {_meshtastic_MessageType_MIN, 0, {0}}
|
||||
#define meshtastic_InterdeviceMessage_init_zero {0, {""}}
|
||||
#define meshtastic_FileTransfer_init_default {_meshtastic_FileOperation_MIN, "", {0, {0}}, _meshtastic_FileStatus_MIN, "", 0, 0, 0}
|
||||
#define meshtastic_DirectoryListing_init_default {"", 0, {"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""}, _meshtastic_FileStatus_MIN, "", 0, 0}
|
||||
#define meshtastic_I2CTransaction_init_default {0, {0, {0}}, 0}
|
||||
#define meshtastic_SdCardInfo_init_default {0, _meshtastic_SdCardInfo_CardType_MIN, _meshtastic_SdCardInfo_FatType_MIN, 0, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_I2CResult_init_default {_meshtastic_I2CResult_Status_MIN, {0, {0}}}
|
||||
#define meshtastic_InterdeviceMessage_init_default {0, {""}, 0}
|
||||
#define meshtastic_FileTransfer_init_zero {_meshtastic_FileOperation_MIN, "", {0, {0}}, _meshtastic_FileStatus_MIN, "", 0, 0, 0}
|
||||
#define meshtastic_DirectoryListing_init_zero {"", 0, {"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""}, _meshtastic_FileStatus_MIN, "", 0, 0}
|
||||
#define meshtastic_I2CTransaction_init_zero {0, {0, {0}}, 0}
|
||||
#define meshtastic_SdCardInfo_init_zero {0, _meshtastic_SdCardInfo_CardType_MIN, _meshtastic_SdCardInfo_FatType_MIN, 0, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_I2CResult_init_zero {_meshtastic_I2CResult_Status_MIN, {0, {0}}}
|
||||
#define meshtastic_InterdeviceMessage_init_zero {0, {""}, 0}
|
||||
|
||||
/* Field tags (for use in manual encoding/decoding) */
|
||||
#define meshtastic_SensorData_type_tag 1
|
||||
#define meshtastic_SensorData_float_value_tag 2
|
||||
#define meshtastic_SensorData_uint32_value_tag 3
|
||||
#define meshtastic_FileTransfer_operation_tag 1
|
||||
#define meshtastic_FileTransfer_filepath_tag 2
|
||||
#define meshtastic_FileTransfer_filedata_tag 3
|
||||
#define meshtastic_FileTransfer_status_tag 4
|
||||
#define meshtastic_FileTransfer_message_tag 5
|
||||
#define meshtastic_FileTransfer_offset_tag 6
|
||||
#define meshtastic_FileTransfer_length_tag 7
|
||||
#define meshtastic_FileTransfer_file_size_tag 8
|
||||
#define meshtastic_DirectoryListing_directory_tag 1
|
||||
#define meshtastic_DirectoryListing_filenames_tag 2
|
||||
#define meshtastic_DirectoryListing_status_tag 3
|
||||
#define meshtastic_DirectoryListing_message_tag 4
|
||||
#define meshtastic_DirectoryListing_offset_tag 5
|
||||
#define meshtastic_DirectoryListing_total_count_tag 6
|
||||
#define meshtastic_I2CTransaction_address_tag 1
|
||||
#define meshtastic_I2CTransaction_write_data_tag 2
|
||||
#define meshtastic_I2CTransaction_read_len_tag 3
|
||||
#define meshtastic_SdCardInfo_present_tag 1
|
||||
#define meshtastic_SdCardInfo_card_type_tag 2
|
||||
#define meshtastic_SdCardInfo_fat_type_tag 3
|
||||
#define meshtastic_SdCardInfo_card_size_tag 4
|
||||
#define meshtastic_SdCardInfo_used_bytes_tag 5
|
||||
#define meshtastic_SdCardInfo_free_bytes_tag 6
|
||||
#define meshtastic_SdCardInfo_stats_valid_tag 7
|
||||
#define meshtastic_SdCardInfo_busy_tag 8
|
||||
#define meshtastic_SdCardInfo_unformatted_tag 9
|
||||
#define meshtastic_I2CResult_status_tag 1
|
||||
#define meshtastic_I2CResult_read_data_tag 2
|
||||
#define meshtastic_InterdeviceMessage_nmea_tag 1
|
||||
#define meshtastic_InterdeviceMessage_sensor_tag 2
|
||||
#define meshtastic_InterdeviceMessage_beep_tag 2
|
||||
#define meshtastic_InterdeviceMessage_i2c_transaction_tag 3
|
||||
#define meshtastic_InterdeviceMessage_i2c_result_tag 4
|
||||
#define meshtastic_InterdeviceMessage_i2c_scan_tag 5
|
||||
#define meshtastic_InterdeviceMessage_i2c_scan_result_tag 6
|
||||
#define meshtastic_InterdeviceMessage_file_transfer_tag 7
|
||||
#define meshtastic_InterdeviceMessage_directory_listing_tag 8
|
||||
#define meshtastic_InterdeviceMessage_get_sd_info_tag 9
|
||||
#define meshtastic_InterdeviceMessage_sd_info_tag 10
|
||||
#define meshtastic_InterdeviceMessage_ping_tag 11
|
||||
#define meshtastic_InterdeviceMessage_pong_tag 12
|
||||
#define meshtastic_InterdeviceMessage_nack_tag 13
|
||||
#define meshtastic_InterdeviceMessage_sd_command_tag 14
|
||||
#define meshtastic_InterdeviceMessage_id_tag 15
|
||||
|
||||
/* Struct field encoding specification for nanopb */
|
||||
#define meshtastic_SensorData_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UENUM, type, 1) \
|
||||
X(a, STATIC, ONEOF, FLOAT, (data,float_value,data.float_value), 2) \
|
||||
X(a, STATIC, ONEOF, UINT32, (data,uint32_value,data.uint32_value), 3)
|
||||
#define meshtastic_SensorData_CALLBACK NULL
|
||||
#define meshtastic_SensorData_DEFAULT NULL
|
||||
#define meshtastic_FileTransfer_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UENUM, operation, 1) \
|
||||
X(a, STATIC, SINGULAR, STRING, filepath, 2) \
|
||||
X(a, STATIC, SINGULAR, BYTES, filedata, 3) \
|
||||
X(a, STATIC, SINGULAR, UENUM, status, 4) \
|
||||
X(a, STATIC, SINGULAR, STRING, message, 5) \
|
||||
X(a, STATIC, SINGULAR, UINT64, offset, 6) \
|
||||
X(a, STATIC, SINGULAR, UINT32, length, 7) \
|
||||
X(a, STATIC, SINGULAR, UINT64, file_size, 8)
|
||||
#define meshtastic_FileTransfer_CALLBACK NULL
|
||||
#define meshtastic_FileTransfer_DEFAULT NULL
|
||||
|
||||
#define meshtastic_DirectoryListing_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, STRING, directory, 1) \
|
||||
X(a, STATIC, REPEATED, STRING, filenames, 2) \
|
||||
X(a, STATIC, SINGULAR, UENUM, status, 3) \
|
||||
X(a, STATIC, SINGULAR, STRING, message, 4) \
|
||||
X(a, STATIC, SINGULAR, UINT32, offset, 5) \
|
||||
X(a, STATIC, SINGULAR, UINT32, total_count, 6)
|
||||
#define meshtastic_DirectoryListing_CALLBACK NULL
|
||||
#define meshtastic_DirectoryListing_DEFAULT NULL
|
||||
|
||||
#define meshtastic_I2CTransaction_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UINT32, address, 1) \
|
||||
X(a, STATIC, SINGULAR, BYTES, write_data, 2) \
|
||||
X(a, STATIC, SINGULAR, UINT32, read_len, 3)
|
||||
#define meshtastic_I2CTransaction_CALLBACK NULL
|
||||
#define meshtastic_I2CTransaction_DEFAULT NULL
|
||||
|
||||
#define meshtastic_SdCardInfo_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, BOOL, present, 1) \
|
||||
X(a, STATIC, SINGULAR, UENUM, card_type, 2) \
|
||||
X(a, STATIC, SINGULAR, UENUM, fat_type, 3) \
|
||||
X(a, STATIC, SINGULAR, UINT64, card_size, 4) \
|
||||
X(a, STATIC, SINGULAR, UINT64, used_bytes, 5) \
|
||||
X(a, STATIC, SINGULAR, UINT64, free_bytes, 6) \
|
||||
X(a, STATIC, SINGULAR, BOOL, stats_valid, 7) \
|
||||
X(a, STATIC, SINGULAR, BOOL, busy, 8) \
|
||||
X(a, STATIC, SINGULAR, BOOL, unformatted, 9)
|
||||
#define meshtastic_SdCardInfo_CALLBACK NULL
|
||||
#define meshtastic_SdCardInfo_DEFAULT NULL
|
||||
|
||||
#define meshtastic_I2CResult_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UENUM, status, 1) \
|
||||
X(a, STATIC, SINGULAR, BYTES, read_data, 2)
|
||||
#define meshtastic_I2CResult_CALLBACK NULL
|
||||
#define meshtastic_I2CResult_DEFAULT NULL
|
||||
|
||||
#define meshtastic_InterdeviceMessage_FIELDLIST(X, a) \
|
||||
X(a, STATIC, ONEOF, STRING, (data,nmea,data.nmea), 1) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (data,sensor,data.sensor), 2)
|
||||
X(a, STATIC, ONEOF, UINT32, (data,beep,data.beep), 2) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (data,i2c_transaction,data.i2c_transaction), 3) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (data,i2c_result,data.i2c_result), 4) \
|
||||
X(a, STATIC, ONEOF, BOOL, (data,i2c_scan,data.i2c_scan), 5) \
|
||||
X(a, STATIC, ONEOF, BYTES, (data,i2c_scan_result,data.i2c_scan_result), 6) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (data,file_transfer,data.file_transfer), 7) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (data,directory_listing,data.directory_listing), 8) \
|
||||
X(a, STATIC, ONEOF, BOOL, (data,get_sd_info,data.get_sd_info), 9) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (data,sd_info,data.sd_info), 10) \
|
||||
X(a, STATIC, ONEOF, UENUM, (data,ping,data.ping), 11) \
|
||||
X(a, STATIC, ONEOF, UENUM, (data,pong,data.pong), 12) \
|
||||
X(a, STATIC, ONEOF, BOOL, (data,nack,data.nack), 13) \
|
||||
X(a, STATIC, ONEOF, UENUM, (data,sd_command,data.sd_command), 14) \
|
||||
X(a, STATIC, SINGULAR, UINT32, id, 15)
|
||||
#define meshtastic_InterdeviceMessage_CALLBACK NULL
|
||||
#define meshtastic_InterdeviceMessage_DEFAULT NULL
|
||||
#define meshtastic_InterdeviceMessage_data_sensor_MSGTYPE meshtastic_SensorData
|
||||
#define meshtastic_InterdeviceMessage_data_i2c_transaction_MSGTYPE meshtastic_I2CTransaction
|
||||
#define meshtastic_InterdeviceMessage_data_i2c_result_MSGTYPE meshtastic_I2CResult
|
||||
#define meshtastic_InterdeviceMessage_data_file_transfer_MSGTYPE meshtastic_FileTransfer
|
||||
#define meshtastic_InterdeviceMessage_data_directory_listing_MSGTYPE meshtastic_DirectoryListing
|
||||
#define meshtastic_InterdeviceMessage_data_sd_info_MSGTYPE meshtastic_SdCardInfo
|
||||
|
||||
extern const pb_msgdesc_t meshtastic_SensorData_msg;
|
||||
extern const pb_msgdesc_t meshtastic_FileTransfer_msg;
|
||||
extern const pb_msgdesc_t meshtastic_DirectoryListing_msg;
|
||||
extern const pb_msgdesc_t meshtastic_I2CTransaction_msg;
|
||||
extern const pb_msgdesc_t meshtastic_SdCardInfo_msg;
|
||||
extern const pb_msgdesc_t meshtastic_I2CResult_msg;
|
||||
extern const pb_msgdesc_t meshtastic_InterdeviceMessage_msg;
|
||||
|
||||
/* Defines for backwards compatibility with code written before nanopb-0.4.0 */
|
||||
#define meshtastic_SensorData_fields &meshtastic_SensorData_msg
|
||||
#define meshtastic_FileTransfer_fields &meshtastic_FileTransfer_msg
|
||||
#define meshtastic_DirectoryListing_fields &meshtastic_DirectoryListing_msg
|
||||
#define meshtastic_I2CTransaction_fields &meshtastic_I2CTransaction_msg
|
||||
#define meshtastic_SdCardInfo_fields &meshtastic_SdCardInfo_msg
|
||||
#define meshtastic_I2CResult_fields &meshtastic_I2CResult_msg
|
||||
#define meshtastic_InterdeviceMessage_fields &meshtastic_InterdeviceMessage_msg
|
||||
|
||||
/* Maximum encoded size of messages (where known) */
|
||||
#define MESHTASTIC_MESHTASTIC_INTERDEVICE_PB_H_MAX_SIZE meshtastic_InterdeviceMessage_size
|
||||
#define meshtastic_InterdeviceMessage_size 1026
|
||||
#define meshtastic_SensorData_size 9
|
||||
#define meshtastic_DirectoryListing_size 4657
|
||||
#define meshtastic_FileTransfer_size 4646
|
||||
#define meshtastic_I2CResult_size 261
|
||||
#define meshtastic_I2CTransaction_size 271
|
||||
#define meshtastic_InterdeviceMessage_size 4666
|
||||
#define meshtastic_SdCardInfo_size 45
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user