Merge branch 'meshtastic:develop' into develop
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.
|
**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.
|
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`
|
- **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`
|
- **Serial sessions** (long-running, 10k-line ring buffer): `serial_open`, `serial_read`, `serial_list`, `serial_close`
|
||||||
- **Device reads**: `device_info`, `list_nodes`
|
- **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`
|
- **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`
|
- **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.
|
- **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".
|
**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)
|
### 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.
|
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.
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
pio upgrade
|
pio upgrade
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
|
|
||||||
|
|||||||
@@ -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`
|
- **Serial sessions**: `serial_open`, `serial_read`, `serial_list`, `serial_close`
|
||||||
- **Device reads**: `device_info`, `list_nodes`
|
- **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`
|
- **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`
|
- **userPrefs admin**: `userprefs_get`, `userprefs_set`, `userprefs_reset`, `userprefs_manifest`, `userprefs_testing_profile`
|
||||||
- **Vendor escape hatches**: `esptool_*`, `nrfutil_*`, `picotool_*`
|
- **Vendor escape hatches**: `esptool_*`, `nrfutil_*`, `picotool_*`
|
||||||
|
|
||||||
|
|||||||
+24
-1
@@ -153,6 +153,30 @@ Display:
|
|||||||
### You can also specify the spi device for the display to use
|
### You can also specify the spi device for the display to use
|
||||||
# spidev: spidev0.0
|
# 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:
|
Touchscreen:
|
||||||
### Note, at least for now, the touchscreen must have a CS pin defined, even if you let Linux manage the CS switching.
|
### Note, at least for now, the touchscreen must have a CS pin defined, even if you let Linux manage the CS switching.
|
||||||
|
|
||||||
@@ -167,7 +191,6 @@ Touchscreen:
|
|||||||
### You can also specify the spi device for the touchscreen to use
|
### You can also specify the spi device for the touchscreen to use
|
||||||
# spidev: spidev0.0
|
# spidev: spidev0.0
|
||||||
|
|
||||||
|
|
||||||
Input:
|
Input:
|
||||||
### Configure device for direct keyboard input
|
### Configure device for direct keyboard input
|
||||||
|
|
||||||
|
|||||||
@@ -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,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
|
||||||
@@ -340,6 +340,18 @@ for lb in env.GetLibBuilders():
|
|||||||
lb.env.Append(CPPDEFINES=[("APP_VERSION", verObj["long"])])
|
lb.env.Append(CPPDEFINES=[("APP_VERSION", verObj["long"])])
|
||||||
break
|
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
|
# Get the display resolution from macros
|
||||||
def get_display_resolution(build_flags):
|
def get_display_resolution(build_flags):
|
||||||
# Check "DISPLAY_SIZE" to determine the screen resolution
|
# Check "DISPLAY_SIZE" to determine the screen resolution
|
||||||
|
|||||||
@@ -257,14 +257,14 @@ so they are stable as listed here:
|
|||||||
|
|
||||||
`region_groups` (region → group_index):
|
`region_groups` (region → group_index):
|
||||||
|
|
||||||
| group | regions |
|
| 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 |
|
| 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 |
|
| 1 | EU_868 |
|
||||||
| 2 | EU_866 |
|
| 2 | EU_866 |
|
||||||
| 3 | EU_N_868 |
|
| 3 | EU_N_868 |
|
||||||
| 4 | ITU1_2M, ITU2_2M, ITU3_2M |
|
| 4 | ITU1_2M, ITU2_2M, ITU3_2M |
|
||||||
| 5 | ITU2_125CM |
|
| 5 | ITU2_125CM |
|
||||||
|
|
||||||
> Note groups **3** and **5** carry the same preset list (NARROW\_\*) but are distinct groups
|
> 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
|
> because they differ in `licensed_only`. Decoders must key on the group, not on the preset
|
||||||
|
|||||||
+3
-3
@@ -69,7 +69,7 @@ monitor_speed = 115200
|
|||||||
monitor_filters = direct
|
monitor_filters = direct
|
||||||
lib_deps =
|
lib_deps =
|
||||||
# renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master
|
# 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
|
# renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master
|
||||||
https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip
|
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
|
# 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]
|
[device-ui_base]
|
||||||
lib_deps =
|
lib_deps =
|
||||||
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
|
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
|
||||||
https://github.com/meshtastic/device-ui/archive/effbb925a7cbd3ab794dfcc5fa16228217d18408.zip
|
https://github.com/meshtastic/device-ui/archive/33917d583eb3f4d6f93a24122a88101221afcc2d.zip
|
||||||
|
|
||||||
; Common libs for environmental measurements in telemetry module
|
; Common libs for environmental measurements in telemetry module
|
||||||
[environmental_base]
|
[environmental_base]
|
||||||
@@ -191,7 +191,7 @@ lib_deps =
|
|||||||
# renovate: datasource=github-tags depName=Adafruit LTR390 Library packageName=adafruit/Adafruit_LTR390
|
# 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
|
https://github.com/adafruit/Adafruit_LTR390/archive/refs/tags/1.1.2.zip
|
||||||
# renovate: datasource=github-tags depName=Adafruit PCT2075 packageName=adafruit/Adafruit_PCT2075
|
# 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
|
# renovate: datasource=github-tags depName=DFRobot_BMM150 packageName=dfrobot/DFRobot_BMM150
|
||||||
https://github.com/DFRobot/DFRobot_BMM150/archive/refs/tags/V1.0.0.zip
|
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
|
# renovate: datasource=github-tags depName=SparkFun MMC5983MA Magnetometer packageName=sparkfun/SparkFun_MMC5983MA_Magnetometer_Arduino_Library
|
||||||
|
|||||||
+1
-1
Submodule protobufs updated: 9d589c1321...f5b94bc367
@@ -57,7 +57,6 @@ using namespace Adafruit_LittleFS_Namespace;
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
void fsInit();
|
void fsInit();
|
||||||
void fsListFiles();
|
|
||||||
bool copyFile(const char *from, const char *to);
|
bool copyFile(const char *from, const char *to);
|
||||||
bool renameFile(const char *pathFrom, const char *pathTo);
|
bool renameFile(const char *pathFrom, const char *pathTo);
|
||||||
bool fsFormat();
|
bool fsFormat();
|
||||||
|
|||||||
+1
-1
@@ -906,7 +906,7 @@ void Power::shutdown()
|
|||||||
#if HAS_SCREEN
|
#if HAS_SCREEN
|
||||||
messageStore.saveToFlash();
|
messageStore.saveToFlash();
|
||||||
#endif
|
#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
|
#ifdef PIN_LED1
|
||||||
ledOff(PIN_LED1);
|
ledOff(PIN_LED1);
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -51,9 +51,6 @@ class PowerStatus : public Status
|
|||||||
|
|
||||||
bool getHasUSB() const { return hasUSB == OptTrue; }
|
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; }
|
bool getIsCharging() const { return isCharging == OptTrue; }
|
||||||
|
|
||||||
int getBatteryVoltageMv() const { return batteryVoltageMv; }
|
int getBatteryVoltageMv() const { return batteryVoltageMv; }
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#include "RedirectablePrint.h"
|
#include "RedirectablePrint.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "RTC.h"
|
|
||||||
#include "concurrency/OSThread.h"
|
#include "concurrency/OSThread.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "memGet.h"
|
#include "memGet.h"
|
||||||
#include "mesh/generated/meshtastic/mesh.pb.h"
|
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||||
|
|||||||
@@ -30,6 +30,16 @@
|
|||||||
|
|
||||||
SerialConsole *console;
|
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.
|
/// Create the shared serial console once and register receive wakeups.
|
||||||
void consoleInit()
|
void consoleInit()
|
||||||
{
|
{
|
||||||
@@ -88,6 +98,30 @@ SerialConsole::SerialConsole() : StreamAPI(&Port), RedirectablePrint(&Port), con
|
|||||||
/// Service one serial API iteration and select the next polling interval.
|
/// Service one serial API iteration and select the next polling interval.
|
||||||
int32_t SerialConsole::runOnce()
|
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
|
#ifdef HELTEC_MESH_SOLAR
|
||||||
// After enabling the mesh solar serial port module configuration, command processing is handled by the serial port module.
|
// 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 &&
|
if (moduleConfig.serial.enabled && moduleConfig.serial.override_console_serial_port &&
|
||||||
|
|||||||
@@ -88,6 +88,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#define MESHTASTIC_PREHOP_DROP 1
|
#define MESHTASTIC_PREHOP_DROP 1
|
||||||
#endif
|
#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
|
/// Convert a preprocessor name into a quoted string
|
||||||
#define xstr(s) ystr(s)
|
#define xstr(s) ystr(s)
|
||||||
#define ystr(s) #s
|
#define ystr(s) #s
|
||||||
@@ -416,6 +424,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#ifndef HAS_TFT
|
#ifndef HAS_TFT
|
||||||
#define HAS_TFT 0
|
#define HAS_TFT 0
|
||||||
#endif
|
#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
|
#ifndef HAS_WIRE
|
||||||
#define HAS_WIRE 0
|
#define HAS_WIRE 0
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+1
-1
@@ -8,10 +8,10 @@
|
|||||||
#include "GpioLogic.h"
|
#include "GpioLogic.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "PowerMon.h"
|
#include "PowerMon.h"
|
||||||
#include "RTC.h"
|
|
||||||
#include "Throttle.h"
|
#include "Throttle.h"
|
||||||
#include "buzz.h"
|
#include "buzz.h"
|
||||||
#include "concurrency/Periodic.h"
|
#include "concurrency/Periodic.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
#include "meshUtils.h"
|
#include "meshUtils.h"
|
||||||
|
|
||||||
#include "main.h" // pmu_found
|
#include "main.h" // pmu_found
|
||||||
|
|||||||
+38
-5
@@ -36,19 +36,52 @@ GeoCoord::GeoCoord(double lat, double lon, int32_t alt) : _altitude(alt)
|
|||||||
GeoCoord::setCoords();
|
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()
|
void GeoCoord::setCoords()
|
||||||
{
|
{
|
||||||
double lat = _latitude * 1e-7;
|
double lat = _latitude * 1e-7;
|
||||||
double lon = _longitude * 1e-7;
|
double lon = _longitude * 1e-7;
|
||||||
GeoCoord::latLongToDMS(lat, lon, _dms);
|
GeoCoord::latLongToDMS(lat, lon, _dms);
|
||||||
GeoCoord::latLongToUTM(lat, lon, _utm);
|
_utmValid = false;
|
||||||
GeoCoord::latLongToMGRS(lat, lon, _mgrs);
|
_mgrsValid = false;
|
||||||
GeoCoord::latLongToOSGR(lat, lon, _osgr);
|
_osgrValid = false;
|
||||||
GeoCoord::latLongToOLC(lat, lon, _olc);
|
_olcValid = false;
|
||||||
_dirty = 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)
|
void GeoCoord::updateCoords(int32_t lat, int32_t lon, int32_t alt)
|
||||||
{
|
{
|
||||||
// If marked dirty or new coordinates
|
// If marked dirty or new coordinates
|
||||||
|
|||||||
+93
-23
@@ -65,14 +65,24 @@ class GeoCoord
|
|||||||
int32_t _altitude = 0;
|
int32_t _altitude = 0;
|
||||||
|
|
||||||
DMS _dms = {};
|
DMS _dms = {};
|
||||||
UTM _utm = {};
|
// Computed lazily on first access via ensure*() below; mutable so const getters can populate
|
||||||
MGRS _mgrs = {};
|
// them on demand.
|
||||||
OSGR _osgr = {};
|
mutable UTM _utm = {};
|
||||||
OLC _olc = {};
|
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;
|
bool _dirty = true;
|
||||||
|
|
||||||
void setCoords();
|
void setCoords();
|
||||||
|
void ensureUTM() const;
|
||||||
|
void ensureMGRS() const;
|
||||||
|
void ensureOSGR() const;
|
||||||
|
void ensureOLC() const;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
GeoCoord();
|
GeoCoord();
|
||||||
@@ -123,26 +133,86 @@ class GeoCoord
|
|||||||
uint32_t getDMSLonSec() const { return _dms.lonSec; }
|
uint32_t getDMSLonSec() const { return _dms.lonSec; }
|
||||||
char getDMSLonCP() const { return _dms.lonCP; }
|
char getDMSLonCP() const { return _dms.lonCP; }
|
||||||
|
|
||||||
// UTM getters
|
// UTM getters (computed on first access - see ensureUTM())
|
||||||
uint8_t getUTMZone() const { return _utm.zone; }
|
uint8_t getUTMZone() const
|
||||||
char getUTMBand() const { return _utm.band; }
|
{
|
||||||
uint32_t getUTMEasting() const { return _utm.easting; }
|
ensureUTM();
|
||||||
uint32_t getUTMNorthing() const { return _utm.northing; }
|
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
|
// MGRS getters (computed on first access - see ensureMGRS())
|
||||||
uint8_t getMGRSZone() const { return _mgrs.zone; }
|
uint8_t getMGRSZone() const
|
||||||
char getMGRSBand() const { return _mgrs.band; }
|
{
|
||||||
char getMGRSEast100k() const { return _mgrs.east100k; }
|
ensureMGRS();
|
||||||
char getMGRSNorth100k() const { return _mgrs.north100k; }
|
return _mgrs.zone;
|
||||||
uint32_t getMGRSEasting() const { return _mgrs.easting; }
|
}
|
||||||
uint32_t getMGRSNorthing() const { return _mgrs.northing; }
|
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
|
// OSGR getters (computed on first access - see ensureOSGR())
|
||||||
char getOSGRE100k() const { return _osgr.e100k; }
|
char getOSGRE100k() const
|
||||||
char getOSGRN100k() const { return _osgr.n100k; }
|
{
|
||||||
uint32_t getOSGREasting() const { return _osgr.easting; }
|
ensureOSGR();
|
||||||
uint32_t getOSGRNorthing() const { return _osgr.northing; }
|
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
|
// OLC getter (computed on first access - see ensureOLC())
|
||||||
void getOLCCode(char *code) { strncpy(code, _olc.code, OLC_CODE_LEN + 1); } // +1 for null termination
|
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
|
#if !MESHTASTIC_EXCLUDE_GPS
|
||||||
#include "NMEAWPL.h"
|
#include "NMEAWPL.h"
|
||||||
#include "GeoCoord.h"
|
#include "GeoCoord.h"
|
||||||
#include "RTC.h"
|
#include "gps/RTC.h"
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
|
||||||
/* -------------------------------------------
|
/* -------------------------------------------
|
||||||
|
|||||||
+33
-1
@@ -1,4 +1,4 @@
|
|||||||
#include "RTC.h"
|
#include "gps/RTC.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#include "detect/ScanI2C.h"
|
#include "detect/ScanI2C.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
@@ -7,6 +7,10 @@
|
|||||||
#include <sys/time.h>
|
#include <sys/time.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
|
||||||
|
#if HAS_LSE
|
||||||
|
#include <STM32RTC.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
static RTCQuality currentQuality = RTCQualityNone;
|
static RTCQuality currentQuality = RTCQualityNone;
|
||||||
uint32_t lastSetFromPhoneNtpOrGps = 0;
|
uint32_t lastSetFromPhoneNtpOrGps = 0;
|
||||||
|
|
||||||
@@ -211,6 +215,30 @@ RTCSetResult readFromRTC()
|
|||||||
return RTCSetResultSuccess;
|
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
|
#else
|
||||||
return readFromSystemTimeFallback();
|
return readFromSystemTimeFallback();
|
||||||
#endif
|
#endif
|
||||||
@@ -335,6 +363,10 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
|
|||||||
LOG_WARN("Failed to set time for RX8130CE");
|
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)
|
#elif defined(ARCH_ESP32) || defined(ARCH_RP2040)
|
||||||
settimeofday(tv, NULL);
|
settimeofday(tv, NULL);
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -8,6 +8,11 @@
|
|||||||
#include <ArtronShop_RX8130CE.h>
|
#include <ArtronShop_RX8130CE.h>
|
||||||
#endif
|
#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 {
|
enum RTCQuality {
|
||||||
|
|
||||||
/// We haven't had our RTC set yet
|
/// We haven't had our RTC set yet
|
||||||
|
|||||||
@@ -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
|
||||||
+104
-3
@@ -29,6 +29,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#if HAS_SCREEN
|
#if HAS_SCREEN
|
||||||
#include "EInkParallelDisplay.h"
|
#include "EInkParallelDisplay.h"
|
||||||
#include <OLEDDisplay.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 "DisplayFormatters.h"
|
||||||
#include "TimeFormatters.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/Default.h"
|
||||||
#include "mesh/generated/meshtastic/deviceonly.pb.h"
|
#include "mesh/generated/meshtastic/deviceonly.pb.h"
|
||||||
#include "modules/ExternalNotificationModule.h"
|
#include "modules/ExternalNotificationModule.h"
|
||||||
|
#if BASEUI_HAS_GAMES
|
||||||
|
#include "modules/games/GamesModule.h"
|
||||||
|
#endif
|
||||||
#include "modules/WaypointModule.h"
|
#include "modules/WaypointModule.h"
|
||||||
#include "sleep.h"
|
#include "sleep.h"
|
||||||
#include "target_specific.h"
|
#include "target_specific.h"
|
||||||
@@ -99,7 +108,11 @@ namespace graphics
|
|||||||
#define COMPASS_ACTIVE_FRAMERATE 20
|
#define COMPASS_ACTIVE_FRAMERATE 20
|
||||||
|
|
||||||
// DEBUG
|
// 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
|
#define NUM_EXTRA_FRAMES 3 // text message and debug frame
|
||||||
|
#endif
|
||||||
// if defined a pixel will blink to show redraws
|
// if defined a pixel will blink to show redraws
|
||||||
// #define SHOW_REDRAWS
|
// #define SHOW_REDRAWS
|
||||||
#define ASCII_BELL '\x07'
|
#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
|
// 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)
|
std::function<void(uint32_t)> bannerCallback)
|
||||||
{
|
{
|
||||||
#ifdef USE_EINK
|
#ifdef USE_EINK
|
||||||
@@ -356,7 +369,10 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
|
|||||||
NotificationRenderer::alertBannerCallback = bannerCallback;
|
NotificationRenderer::alertBannerCallback = bannerCallback;
|
||||||
NotificationRenderer::pauseBanner = false;
|
NotificationRenderer::pauseBanner = false;
|
||||||
NotificationRenderer::curSelected = 0;
|
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::numDigits = digits;
|
||||||
NotificationRenderer::currentNumber = 0;
|
NotificationRenderer::currentNumber = 0;
|
||||||
|
|
||||||
@@ -366,6 +382,43 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
|
|||||||
updateUiFrame(ui);
|
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,
|
void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||||
std::function<void(const std::string &)> textCallback)
|
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);
|
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.
|
* 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
|
#else
|
||||||
dispdev = new ST7796Spi(&SPI1, ST7796_RESET, ST7796_RS, ST7796_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT);
|
dispdev = new ST7796Spi(&SPI1, ST7796_RESET, ST7796_RS, ST7796_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT);
|
||||||
#endif
|
#endif
|
||||||
|
#elif defined(USE_HUB75)
|
||||||
|
dispdev = new HUB75Display(address.address, -1, -1, GEOMETRY_RAWMODE, HW_I2C::I2C_ONE);
|
||||||
#elif defined(USE_SSD1306)
|
#elif defined(USE_SSD1306)
|
||||||
dispdev = new SSD1306Wire(address.address, -1, -1, geometry,
|
dispdev = new SSD1306Wire(address.address, -1, -1, geometry,
|
||||||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
(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");
|
LOG_INFO("SSD1306 init success");
|
||||||
}
|
}
|
||||||
#elif ARCH_PORTDUINO
|
#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) {
|
if (portduino_config.displayPanel != no_screen) {
|
||||||
LOG_DEBUG("Make TFTDisplay!");
|
LOG_DEBUG("Make TFTDisplay!");
|
||||||
dispdev = new TFTDisplay(address.address, -1, -1, geometry,
|
dispdev = new TFTDisplay(address.address, -1, -1, geometry,
|
||||||
@@ -1301,6 +1377,15 @@ void Screen::setFrames(FrameFocus focus)
|
|||||||
indicatorIcons.push_back(icon_home);
|
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;
|
fsi.positions.textMessage = numframes;
|
||||||
normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame;
|
normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame;
|
||||||
indicatorIcons.push_back(icon_mail);
|
indicatorIcons.push_back(icon_mail);
|
||||||
@@ -2073,6 +2158,12 @@ int Screen::handleInputEvent(const InputEvent *event)
|
|||||||
if (module && module->interceptingKeyboardInput())
|
if (module && module->interceptingKeyboardInput())
|
||||||
inputIntercepted = true;
|
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 no modules are using the input, move between frames
|
||||||
if (!inputIntercepted) {
|
if (!inputIntercepted) {
|
||||||
@@ -2147,6 +2238,11 @@ int Screen::handleInputEvent(const InputEvent *event)
|
|||||||
} else if (event->inputEvent == INPUT_BROKER_SELECT) {
|
} else if (event->inputEvent == INPUT_BROKER_SELECT) {
|
||||||
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
|
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
|
||||||
menuHandler::homeBaseMenu();
|
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) {
|
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.system) {
|
||||||
menuHandler::systemBaseMenu();
|
menuHandler::systemBaseMenu();
|
||||||
#if HAS_GPS
|
#if HAS_GPS
|
||||||
@@ -2214,6 +2310,11 @@ bool Screen::isOverlayBannerShowing()
|
|||||||
return NotificationRenderer::isOverlayBannerShowing();
|
return NotificationRenderer::isOverlayBannerShowing();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Screen::isGamesFrameShown()
|
||||||
|
{
|
||||||
|
return framesetInfo.positions.games != 255 && ui && ui->getUiState()->currentFrame == framesetInfo.positions.games;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace graphics
|
} // namespace graphics
|
||||||
|
|
||||||
#else
|
#else
|
||||||
|
|||||||
+15
-1
@@ -26,6 +26,9 @@ enum notificationTypeEnum {
|
|||||||
// LOCKED frame. Without this, a first-pair on a locked device cannot
|
// LOCKED frame. Without this, a first-pair on a locked device cannot
|
||||||
// complete because the PIN never renders.
|
// complete because the PIN never renders.
|
||||||
pairing_pin,
|
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 {
|
struct BannerOverlayOptions {
|
||||||
@@ -284,6 +287,10 @@ class Screen : public concurrency::OSThread
|
|||||||
|
|
||||||
bool isOverlayBannerShowing();
|
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; }
|
bool isScreenOn() { return screenOn; }
|
||||||
|
|
||||||
// Stores the last 4 of our hardware ID, to make finding the device for pairing easier
|
// 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 showOverlayBanner(BannerOverlayOptions);
|
||||||
|
|
||||||
void showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback);
|
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,
|
void showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||||
std::function<void(const std::string &)> textCallback);
|
std::function<void(const std::string &)> textCallback);
|
||||||
|
|
||||||
@@ -734,6 +747,7 @@ class Screen : public concurrency::OSThread
|
|||||||
uint8_t system = 255;
|
uint8_t system = 255;
|
||||||
uint8_t gps = 255;
|
uint8_t gps = 255;
|
||||||
uint8_t home = 255;
|
uint8_t home = 255;
|
||||||
|
uint8_t games = 255;
|
||||||
uint8_t textMessage = 255;
|
uint8_t textMessage = 255;
|
||||||
uint8_t nodelist_nodes = 255;
|
uint8_t nodelist_nodes = 255;
|
||||||
uint8_t nodelist_location = 255;
|
uint8_t nodelist_location = 255;
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
#include "MeshService.h"
|
#include "MeshService.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "Power.h"
|
#include "Power.h"
|
||||||
#include "RTC.h"
|
|
||||||
#include "draw/NodeListRenderer.h"
|
#include "draw/NodeListRenderer.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
#include "graphics/ScreenFonts.h"
|
#include "graphics/ScreenFonts.h"
|
||||||
#include "graphics/SharedUIDisplay.h"
|
#include "graphics/SharedUIDisplay.h"
|
||||||
#include "graphics/TFTColorRegions.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
|
// Transparent clock headers should inherit whatever body off-color is
|
||||||
// already active under the header (important for light/inverted themes).
|
// already active under the header (important for light/inverted themes).
|
||||||
const uint16_t transparentBgColor = resolveTFTOffColorAt(0, headerHeight + 1, getThemeBodyBg());
|
const uint16_t transparentBgColor = resolveTFTOffColorAt(0, headerHeight + 1, getThemeBodyBg());
|
||||||
setAndRegisterTFTColorRole(TFTColorRole::HeaderBackground, transparentBgColor, transparentBgColor, 0, 0, screenW,
|
// Intentionally skip the HeaderBackground region, as small screens draw the clock in the unused space in this region,
|
||||||
headerHeight);
|
// and the transparent call was erasing segments from the clock
|
||||||
setTFTColorRole(TFTColorRole::HeaderTitle, headerTitleColorForRole, transparentBgColor);
|
setTFTColorRole(TFTColorRole::HeaderTitle, headerTitleColorForRole, transparentBgColor);
|
||||||
setTFTColorRole(TFTColorRole::HeaderStatus, headerStatusColor, transparentBgColor);
|
setTFTColorRole(TFTColorRole::HeaderStatus, headerStatusColor, transparentBgColor);
|
||||||
} else if (useInvertedHeaderStyle) {
|
} else if (useInvertedHeaderStyle) {
|
||||||
|
|||||||
@@ -16,8 +16,9 @@ struct TFTColorRegion {
|
|||||||
// Required by ST7789 driver: it scans until the first disabled entry.
|
// Required by ST7789 driver: it scans until the first disabled entry.
|
||||||
bool enabled = false;
|
bool enabled = false;
|
||||||
};
|
};
|
||||||
|
#ifndef MAX_TFT_COLOR_REGIONS
|
||||||
static constexpr size_t MAX_TFT_COLOR_REGIONS = 48;
|
static constexpr size_t MAX_TFT_COLOR_REGIONS = 48;
|
||||||
|
#endif
|
||||||
extern TFTColorRegion colorRegions[MAX_TFT_COLOR_REGIONS];
|
extern TFTColorRegion colorRegions[MAX_TFT_COLOR_REGIONS];
|
||||||
|
|
||||||
enum class TFTColorRole : uint8_t {
|
enum class TFTColorRole : uint8_t {
|
||||||
@@ -39,7 +40,7 @@ enum class TFTColorRole : uint8_t {
|
|||||||
Count
|
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
|
#define GRAPHICS_TFT_COLORING_ENABLED 1
|
||||||
#else
|
#else
|
||||||
#define GRAPHICS_TFT_COLORING_ENABLED 0
|
#define GRAPHICS_TFT_COLORING_ENABLED 0
|
||||||
|
|||||||
@@ -121,7 +121,6 @@ static void rak14014_tpIntHandle(void)
|
|||||||
|
|
||||||
#elif defined(HACKADAY_COMMUNICATOR)
|
#elif defined(HACKADAY_COMMUNICATOR)
|
||||||
#include <Arduino_GFX_Library.h>
|
#include <Arduino_GFX_Library.h>
|
||||||
Arduino_DataBus *bus = nullptr;
|
|
||||||
Arduino_GFX *tft = nullptr;
|
Arduino_GFX *tft = nullptr;
|
||||||
|
|
||||||
#elif defined(ST72xx_DE)
|
#elif defined(ST72xx_DE)
|
||||||
@@ -1601,17 +1600,21 @@ bool TFTDisplay::connect()
|
|||||||
{
|
{
|
||||||
concurrency::LockGuard g(spiLock);
|
concurrency::LockGuard g(spiLock);
|
||||||
LOG_INFO("Do TFT init");
|
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)
|
#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)
|
#elif defined(HACKADAY_COMMUNICATOR)
|
||||||
bus = new Arduino_ESP32SPI(TFT_DC, TFT_CS, 38 /* SCK */, 21 /* MOSI */, GFX_NOT_DEFINED /* MISO */, HSPI /* spi_num */);
|
Arduino_DataBus *bus =
|
||||||
tft = new Arduino_NV3007(bus, 40, 0 /* rotation */, false /* IPS */, 142 /* width */, 428 /* height */, 12 /* col offset 1 */,
|
new Arduino_ESP32SPI(TFT_DC, TFT_CS, 38 /* SCK */, 21 /* MOSI */, GFX_NOT_DEFINED /* MISO */, HSPI /* spi_num */);
|
||||||
0 /* row offset 1 */, 14 /* col offset 2 */, 0 /* row offset 2 */, nv3007_279_init_operations,
|
tft = new Arduino_NV3007(bus, 40, 0 /* rotation */, false /* IPS */, 142 /* width */, 428 /* height */,
|
||||||
sizeof(nv3007_279_init_operations));
|
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
|
#else
|
||||||
tft = new LGFX;
|
tft = new LGFX;
|
||||||
#endif
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
backlightEnable->set(true);
|
backlightEnable->set(true);
|
||||||
LOG_INFO("Power to TFT Backlight");
|
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->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
int line = 1;
|
int line = 1;
|
||||||
|
int scale = 1;
|
||||||
int iconX = SCREEN_WIDTH - chirpy_width - (chirpy_width / 3);
|
int iconX = SCREEN_WIDTH - chirpy_width - (chirpy_width / 3);
|
||||||
int iconY = (SCREEN_HEIGHT - chirpy_height) / 2;
|
int iconY = (SCREEN_HEIGHT - chirpy_height) / 2;
|
||||||
int textX_offset = 10;
|
int textX_offset = 10;
|
||||||
if (currentResolution == ScreenResolution::High) {
|
if (currentResolution == ScreenResolution::High) {
|
||||||
textX_offset = textX_offset * 4;
|
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;
|
const int bytesPerRow = (chirpy_width + 7) / 8;
|
||||||
|
|
||||||
for (int yy = 0; yy < chirpy_height; ++yy) {
|
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;
|
const uint8_t *rowPtr = chirpy + yy * bytesPerRow;
|
||||||
for (int xx = 0; xx < chirpy_width; ++xx) {
|
for (int xx = 0; xx < chirpy_width; ++xx) {
|
||||||
const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3));
|
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);
|
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);
|
int textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("Hello") / 2);
|
||||||
display->drawString(textX, getTextPositions(display)[line++], "Hello");
|
display->drawString(textX, getTextPositions(display)[line++], "Hello");
|
||||||
textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("World!") / 2);
|
textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("World!") / 2);
|
||||||
|
|||||||
@@ -242,7 +242,6 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
|||||||
{"TH", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_TH},
|
{"TH", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_TH},
|
||||||
{"LORA_24", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_LORA_24},
|
{"LORA_24", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_LORA_24},
|
||||||
{"UA_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_UA_433},
|
{"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_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_MY_433},
|
||||||
{"MY_919", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_MY_919},
|
{"MY_919", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_MY_919},
|
||||||
{"SG_923", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_SG_923},
|
{"SG_923", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_SG_923},
|
||||||
@@ -2323,8 +2322,10 @@ void menuHandler::testMenu()
|
|||||||
|
|
||||||
void menuHandler::numberTest()
|
void menuHandler::numberTest()
|
||||||
{
|
{
|
||||||
screen->showNumberPicker("Pick a number\n ", 30000, 4,
|
screen->showNumberPicker("Verify Nodenum:\n ", 30000, 8, true, [](uint32_t number_picked) -> void {
|
||||||
[](int number_picked) -> void { LOG_WARN("Nodenum: %u", number_picked); });
|
LOG_DEBUG("Nodenum: 0x%08x", number_picked);
|
||||||
|
keyVerificationModule->sendInitialRequest(number_picked);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void menuHandler::wifiBaseMenu()
|
void menuHandler::wifiBaseMenu()
|
||||||
@@ -2508,8 +2509,7 @@ void menuHandler::keyVerificationFinalPrompt()
|
|||||||
options.notificationType = graphics::notificationTypeEnum::selection_picker;
|
options.notificationType = graphics::notificationTypeEnum::selection_picker;
|
||||||
options.bannerCallback = [=](int selected) {
|
options.bannerCallback = [=](int selected) {
|
||||||
if (selected == 1) {
|
if (selected == 1) {
|
||||||
auto remoteNodePtr = nodeDB->getMeshNode(keyVerificationModule->getCurrentRemoteNode());
|
keyVerificationModule->commitVerifiedRemoteNode();
|
||||||
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
screen->showOverlayBanner(options);
|
screen->showOverlayBanner(options);
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ bool NotificationRenderer::pauseBanner = false;
|
|||||||
notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none;
|
notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none;
|
||||||
uint32_t NotificationRenderer::numDigits = 0;
|
uint32_t NotificationRenderer::numDigits = 0;
|
||||||
uint32_t NotificationRenderer::currentNumber = 0;
|
uint32_t NotificationRenderer::currentNumber = 0;
|
||||||
|
char NotificationRenderer::alphanumericValue[16] = {0};
|
||||||
VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr;
|
VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr;
|
||||||
std::function<void(const std::string &)> NotificationRenderer::textInputCallback = nullptr;
|
std::function<void(const std::string &)> NotificationRenderer::textInputCallback = nullptr;
|
||||||
|
|
||||||
@@ -277,6 +278,9 @@ void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayU
|
|||||||
case notificationTypeEnum::hex_picker:
|
case notificationTypeEnum::hex_picker:
|
||||||
drawHexPicker(display, state);
|
drawHexPicker(display, state);
|
||||||
break;
|
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);
|
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)
|
void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||||
{
|
{
|
||||||
static uint32_t selectedNodenum = 0;
|
static uint32_t selectedNodenum = 0;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class NotificationRenderer
|
|||||||
static std::function<void(int)> alertBannerCallback;
|
static std::function<void(int)> alertBannerCallback;
|
||||||
static uint32_t numDigits;
|
static uint32_t numDigits;
|
||||||
static uint32_t currentNumber;
|
static uint32_t currentNumber;
|
||||||
|
static char alphanumericValue[16]; // working buffer for the alphanumeric_picker
|
||||||
static VirtualKeyboard *virtualKeyboard;
|
static VirtualKeyboard *virtualKeyboard;
|
||||||
static std::function<void(const std::string &)> textInputCallback;
|
static std::function<void(const std::string &)> textInputCallback;
|
||||||
|
|
||||||
@@ -43,6 +44,7 @@ class NotificationRenderer
|
|||||||
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||||
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||||
static void drawHexPicker(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 drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||||
static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state);
|
static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||||
static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1],
|
static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1],
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||||
#include "modules/StatusMessageModule.h"
|
#include "modules/StatusMessageModule.h"
|
||||||
#endif
|
#endif
|
||||||
|
#if BASEUI_HAS_GAMES
|
||||||
|
#include "modules/games/GamesModule.h"
|
||||||
|
#endif
|
||||||
#include "UIRenderer.h"
|
#include "UIRenderer.h"
|
||||||
#include "airtime.h"
|
#include "airtime.h"
|
||||||
#include "gps/GeoCoord.h"
|
#include "gps/GeoCoord.h"
|
||||||
@@ -21,8 +24,8 @@
|
|||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "target_specific.h"
|
#include "target_specific.h"
|
||||||
#include <OLEDDisplay.h>
|
#include <OLEDDisplay.h>
|
||||||
#include <RTC.h>
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <gps/RTC.h>
|
||||||
|
|
||||||
// External variables
|
// External variables
|
||||||
extern graphics::Screen *screen;
|
extern graphics::Screen *screen;
|
||||||
@@ -1817,6 +1820,13 @@ constexpr uint32_t ICON_DISPLAY_DURATION_MS = 2000;
|
|||||||
// cppcheck-suppress constParameterPointer; signature must match OverlayCallback typedef from OLEDDisplayUi library
|
// cppcheck-suppress constParameterPointer; signature must match OverlayCallback typedef from OLEDDisplayUi library
|
||||||
void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state)
|
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;
|
uint8_t frameToHighlight = state->currentFrame;
|
||||||
if (state->frameState == IN_TRANSITION && state->transitionFrameTarget < screen->indicatorIcons.size()) {
|
if (state->frameState == IN_TRANSITION && state->transitionFrameTarget < screen->indicatorIcons.size()) {
|
||||||
frameToHighlight = state->transitionFrameTarget;
|
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
|
#define connection_icon_height 5
|
||||||
const uint8_t connection_icon[] = {0x36, 0x41, 0x5D, 0x41, 0x36};
|
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
|
#ifdef OLED_TINY
|
||||||
#include "img/icon_small.xbm"
|
#include "img/icon_small.xbm"
|
||||||
#else
|
#else
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
|
|
||||||
#include "RTC.h"
|
#include "gps/RTC.h"
|
||||||
|
|
||||||
using namespace NicheGraphics;
|
using namespace NicheGraphics;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||||
|
|
||||||
#include "RTC.h"
|
#include "gps/RTC.h"
|
||||||
|
|
||||||
#include "GeoCoord.h"
|
#include "GeoCoord.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ enum MenuAction {
|
|||||||
SET_REGION_TH,
|
SET_REGION_TH,
|
||||||
SET_REGION_LORA_24,
|
SET_REGION_LORA_24,
|
||||||
SET_REGION_UA_433,
|
SET_REGION_UA_433,
|
||||||
SET_REGION_UA_868,
|
|
||||||
SET_REGION_MY_433,
|
SET_REGION_MY_433,
|
||||||
SET_REGION_MY_919,
|
SET_REGION_MY_919,
|
||||||
SET_REGION_SG_923,
|
SET_REGION_SG_923,
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
#include "MeshService.h"
|
#include "MeshService.h"
|
||||||
#include "MessageStore.h"
|
#include "MessageStore.h"
|
||||||
#include "Power.h"
|
#include "Power.h"
|
||||||
#include "RTC.h"
|
|
||||||
#include "Router.h"
|
#include "Router.h"
|
||||||
#include "airtime.h"
|
#include "airtime.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
#include "graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h"
|
#include "graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h"
|
||||||
#include "graphics/niche/Utils/FlashData.h"
|
#include "graphics/niche/Utils/FlashData.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
@@ -835,10 +835,6 @@ void InkHUD::MenuApplet::execute(MenuItem item)
|
|||||||
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_UA_433);
|
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_UA_433);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SET_REGION_UA_868:
|
|
||||||
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_UA_868);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SET_REGION_MY_433:
|
case SET_REGION_MY_433:
|
||||||
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_MY_433);
|
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_MY_433);
|
||||||
break;
|
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("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("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 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 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("MY 919", MenuAction::SET_REGION_MY_919, MenuPage::EXIT));
|
||||||
items.push_back(MenuItem("SG 923", MenuAction::SET_REGION_SG_923, MenuPage::EXIT));
|
items.push_back(MenuItem("SG 923", MenuAction::SET_REGION_SG_923, MenuPage::EXIT));
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
#include "meshUtils.h"
|
#include "meshUtils.h"
|
||||||
#include "modules/TextMessageModule.h"
|
#include "modules/TextMessageModule.h"
|
||||||
|
|
||||||
#include "RTC.h"
|
#include "gps/RTC.h"
|
||||||
|
|
||||||
using namespace NicheGraphics;
|
using namespace NicheGraphics;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||||
|
|
||||||
#include "RTC.h"
|
#include "gps/RTC.h"
|
||||||
|
|
||||||
#include "gps/GeoCoord.h"
|
#include "gps/GeoCoord.h"
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
#include "./RecentsListApplet.h"
|
#include "./RecentsListApplet.h"
|
||||||
|
|
||||||
#include "RTC.h"
|
#include "gps/RTC.h"
|
||||||
|
|
||||||
using namespace NicheGraphics;
|
using namespace NicheGraphics;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
#include "./ThreadedMessageApplet.h"
|
#include "./ThreadedMessageApplet.h"
|
||||||
|
|
||||||
#include "RTC.h"
|
#include "gps/RTC.h"
|
||||||
#include "mesh/NodeDB.h"
|
#include "mesh/NodeDB.h"
|
||||||
|
|
||||||
using namespace NicheGraphics;
|
using namespace NicheGraphics;
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
#include "MessageStore.h"
|
#include "MessageStore.h"
|
||||||
#include "PowerFSM.h"
|
#include "PowerFSM.h"
|
||||||
#include "RTC.h"
|
|
||||||
#include "buzz.h"
|
#include "buzz.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
#include "modules/ExternalNotificationModule.h"
|
#include "modules/ExternalNotificationModule.h"
|
||||||
#include "modules/TextMessageModule.h"
|
#include "modules/TextMessageModule.h"
|
||||||
#include "sleep.h"
|
#include "sleep.h"
|
||||||
|
|||||||
+39
-20
@@ -79,6 +79,16 @@ static int axisZone(int value)
|
|||||||
return 0;
|
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()
|
int32_t LinuxJoystick::runOnce()
|
||||||
{
|
{
|
||||||
if (firstTime) {
|
if (firstTime) {
|
||||||
@@ -107,8 +117,6 @@ int32_t LinuxJoystick::runOnce()
|
|||||||
if (nfds < 0) {
|
if (nfds < 0) {
|
||||||
perror("joystick: epoll_wait failed");
|
perror("joystick: epoll_wait failed");
|
||||||
return disable();
|
return disable();
|
||||||
} else if (nfds == 0) {
|
|
||||||
return 50;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < nfds; i++) {
|
for (int i = 0; i < nfds; i++) {
|
||||||
@@ -117,42 +125,53 @@ int32_t LinuxJoystick::runOnce()
|
|||||||
if (rd < (signed int)sizeof(struct input_event))
|
if (rd < (signed int)sizeof(struct input_event))
|
||||||
continue;
|
continue;
|
||||||
for (int j = 0; j < rd / ((signed int)sizeof(struct input_event)); j++) {
|
for (int j = 0; j < rd / ((signed int)sizeof(struct input_event)); j++) {
|
||||||
InputEvent e = {};
|
|
||||||
e.inputEvent = INPUT_BROKER_NONE;
|
|
||||||
e.source = this->_originName;
|
|
||||||
e.kbchar = 0;
|
|
||||||
unsigned int type = evs[j].type;
|
unsigned int type = evs[j].type;
|
||||||
unsigned int code = evs[j].code;
|
unsigned int code = evs[j].code;
|
||||||
int value = evs[j].value;
|
int value = evs[j].value;
|
||||||
|
|
||||||
if (type == EV_ABS) {
|
if (type == EV_ABS) {
|
||||||
// D-pad reports as ABS_X / ABS_Y with digital 0 / 127 / 255 values.
|
// D-pad reports as ABS_X / ABS_Y with digital 0 / 127 / 255 values. Emit on the
|
||||||
// Emit exactly once when an axis crosses from center to an edge.
|
// transition to an edge and arm auto-repeat; a held direction repeats below.
|
||||||
if (code == ABS_X) {
|
if (code == ABS_X) {
|
||||||
int zone = axisZone(value);
|
int zone = axisZone(value);
|
||||||
if (zone != axisZone(lastX) && zone != 0)
|
if (zone != heldX) {
|
||||||
e.inputEvent = (zone < 0) ? INPUT_BROKER_LEFT : INPUT_BROKER_RIGHT;
|
heldX = zone;
|
||||||
lastX = value;
|
if (zone != 0) {
|
||||||
|
emitEvent((zone < 0) ? INPUT_BROKER_LEFT : INPUT_BROKER_RIGHT);
|
||||||
|
nextRepeatX = millis() + JOY_REPEAT_DELAY_MS;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (code == ABS_Y) {
|
} else if (code == ABS_Y) {
|
||||||
int zone = axisZone(value);
|
int zone = axisZone(value);
|
||||||
if (zone != axisZone(lastY) && zone != 0)
|
if (zone != heldY) {
|
||||||
e.inputEvent = (zone < 0) ? INPUT_BROKER_UP : INPUT_BROKER_DOWN;
|
heldY = zone;
|
||||||
lastY = value;
|
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) {
|
} else if (type == EV_KEY && value == 1) {
|
||||||
// Look up the configured action for this button; unmapped buttons are ignored.
|
// 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);
|
auto mapped = buttonMap.find(code);
|
||||||
if (mapped != buttonMap.end())
|
if (mapped != buttonMap.end())
|
||||||
e.inputEvent = mapped->second;
|
emitEvent(mapped->second);
|
||||||
}
|
|
||||||
|
|
||||||
if (e.inputEvent != INPUT_BROKER_NONE) {
|
|
||||||
LOG_DEBUG("joystick: %s event %d", this->_originName, e.inputEvent);
|
|
||||||
this->notifyObservers(&e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
return 50; // Poll every 50msec
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,10 @@
|
|||||||
#define JOY_AXIS_LOW 64 // below this -> "min" edge (0)
|
#define JOY_AXIS_LOW 64 // below this -> "min" edge (0)
|
||||||
#define JOY_AXIS_HIGH 192 // above this -> "max" edge (255)
|
#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
|
class LinuxJoystick : public Observable<const InputEvent *>, public concurrency::OSThread
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -27,10 +31,18 @@ class LinuxJoystick : public Observable<const InputEvent *>, public concurrency:
|
|||||||
void init(); // Registers this source with the InputBroker
|
void init(); // Registers this source with the InputBroker
|
||||||
void deInit(); // Strictly for cleanly "rebooting" the binary on native
|
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:
|
protected:
|
||||||
virtual int32_t runOnce() override;
|
virtual int32_t runOnce() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void emitEvent(input_broker_event event);
|
||||||
|
|
||||||
const char *_originName;
|
const char *_originName;
|
||||||
bool firstTime = true;
|
bool firstTime = true;
|
||||||
|
|
||||||
@@ -42,10 +54,12 @@ class LinuxJoystick : public Observable<const InputEvent *>, public concurrency:
|
|||||||
int fd = -1;
|
int fd = -1;
|
||||||
int epollfd = -1;
|
int epollfd = -1;
|
||||||
|
|
||||||
// Latch the last emitted axis position so a held direction fires exactly
|
// D-pad auto-repeat state: currently held zone per axis (-1 / 0 / +1) and the
|
||||||
// once (on the transition away from center), not a continuous stream.
|
// next millis() timestamp at which to re-emit while the direction is held.
|
||||||
int lastX = JOY_AXIS_CENTER;
|
int heldX = 0;
|
||||||
int lastY = JOY_AXIS_CENTER;
|
int heldY = 0;
|
||||||
|
uint32_t nextRepeatX = 0;
|
||||||
|
uint32_t nextRepeatY = 0;
|
||||||
};
|
};
|
||||||
extern LinuxJoystick *aLinuxJoystick;
|
extern LinuxJoystick *aLinuxJoystick;
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+5
-1
@@ -22,13 +22,13 @@
|
|||||||
|
|
||||||
#include "FSCommon.h"
|
#include "FSCommon.h"
|
||||||
#include "Power.h"
|
#include "Power.h"
|
||||||
#include "RTC.h"
|
|
||||||
#include "SPILock.h"
|
#include "SPILock.h"
|
||||||
#include "Throttle.h"
|
#include "Throttle.h"
|
||||||
#include "concurrency/OSThread.h"
|
#include "concurrency/OSThread.h"
|
||||||
#include "concurrency/Periodic.h"
|
#include "concurrency/Periodic.h"
|
||||||
#include "detect/ScanI2C.h"
|
#include "detect/ScanI2C.h"
|
||||||
#include "error.h"
|
#include "error.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
|
|
||||||
#if !MESHTASTIC_EXCLUDE_I2C
|
#if !MESHTASTIC_EXCLUDE_I2C
|
||||||
#include "detect/ScanI2CConsumer.h"
|
#include "detect/ScanI2CConsumer.h"
|
||||||
@@ -816,6 +816,10 @@ void setup()
|
|||||||
rp2040Setup();
|
rp2040Setup();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef ARCH_STM32WL
|
||||||
|
stm32wlSetup();
|
||||||
|
#endif
|
||||||
|
|
||||||
// We do this as early as possible because this loads preferences from flash
|
// 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
|
// but we need to do this after main cpu init (esp32setup), because we need the random seed set
|
||||||
nodeDB = new NodeDB;
|
nodeDB = new NodeDB;
|
||||||
|
|||||||
+2
-1
@@ -113,7 +113,8 @@ extern bool runASAP;
|
|||||||
|
|
||||||
extern bool pauseBluetoothLogging;
|
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
|
#ifdef ARCH_ESP32
|
||||||
void esp32ReleaseBluetoothMemoryIfUnused();
|
void esp32ReleaseBluetoothMemoryIfUnused();
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -516,7 +516,7 @@ bool Channels::hasDefaultChannel()
|
|||||||
*/
|
*/
|
||||||
bool Channels::decryptForHash(ChannelIndex chIndex, ChannelHash channelHash)
|
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),
|
// LOG_DEBUG("Skip channel %d (hash %x) due to invalid hash/index, want=%x", chIndex, getHash(chIndex),
|
||||||
// channelHash);
|
// channelHash);
|
||||||
return false;
|
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)
|
uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut)
|
||||||
{
|
{
|
||||||
uint8_t *auth;
|
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;
|
auth = bytesOut + numBytes;
|
||||||
memcpy((uint8_t *)(auth + 8), &extraNonceTmp,
|
memcpy((uint8_t *)(auth + 8), &extraNonceTmp,
|
||||||
sizeof(uint32_t)); // do not use dereference on potential non aligned pointers : *extraNonce = 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;
|
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
|
#endif
|
||||||
concurrency::Lock *cryptLock;
|
concurrency::Lock *cryptLock;
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,17 @@ class CryptoEngine
|
|||||||
virtual bool setDHPublicKey(uint8_t *publicKey);
|
virtual bool setDHPublicKey(uint8_t *publicKey);
|
||||||
virtual void hash(uint8_t *bytes, size_t numBytes);
|
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 aesSetKey(const uint8_t *key, size_t key_len);
|
||||||
|
|
||||||
virtual void aesEncrypt(uint8_t *in, uint8_t *out);
|
virtual void aesEncrypt(uint8_t *in, uint8_t *out);
|
||||||
@@ -94,6 +105,10 @@ class CryptoEngine
|
|||||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||||
uint8_t shared_key[32] = {0};
|
uint8_t shared_key[32] = {0};
|
||||||
uint8_t private_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)
|
#if !(MESHTASTIC_EXCLUDE_XEDDSA)
|
||||||
uint8_t xeddsa_public_key[32] = {0};
|
uint8_t xeddsa_public_key[32] = {0};
|
||||||
uint8_t xeddsa_private_key[32] = {0};
|
uint8_t xeddsa_private_key[32] = {0};
|
||||||
|
|||||||
@@ -11,12 +11,13 @@
|
|||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "Power.h"
|
#include "Power.h"
|
||||||
#include "PowerFSM.h"
|
#include "PowerFSM.h"
|
||||||
#include "RTC.h"
|
|
||||||
#include "TypeConversions.h"
|
#include "TypeConversions.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
#include "graphics/draw/MessageRenderer.h"
|
#include "graphics/draw/MessageRenderer.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "mesh-pb-constants.h"
|
#include "mesh-pb-constants.h"
|
||||||
#include "meshUtils.h"
|
#include "meshUtils.h"
|
||||||
|
#include "modules/AdminModule.h"
|
||||||
#include "modules/NodeInfoModule.h"
|
#include "modules/NodeInfoModule.h"
|
||||||
#include "modules/PositionModule.h"
|
#include "modules/PositionModule.h"
|
||||||
#include "modules/RoutingModule.h"
|
#include "modules/RoutingModule.h"
|
||||||
@@ -63,6 +64,7 @@ Allocator<meshtastic_ClientNotification> &clientNotificationPool = staticClientN
|
|||||||
|
|
||||||
Allocator<meshtastic_QueueStatus> &queueStatusPool = staticQueueStatusPool;
|
Allocator<meshtastic_QueueStatus> &queueStatusPool = staticQueueStatusPool;
|
||||||
|
|
||||||
|
#include "PositionPrecision.h"
|
||||||
#include "Router.h"
|
#include "Router.h"
|
||||||
|
|
||||||
MeshService::MeshService()
|
MeshService::MeshService()
|
||||||
@@ -173,6 +175,51 @@ NodeNum MeshService::getNodenumFromRequestId(uint32_t request_id)
|
|||||||
return nodenum;
|
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)
|
* 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
|
* 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);
|
SimRadio::instance->unpackAndReceive(p);
|
||||||
return;
|
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
|
#endif
|
||||||
p.from = 0; // We don't let clients assign nodenums to their sent messages
|
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
|
p.next_hop = NO_NEXT_HOP_PREFERENCE; // We don't let clients assign next_hop to their sent messages
|
||||||
@@ -203,6 +260,14 @@ void MeshService::handleToRadio(meshtastic_MeshPacket &p)
|
|||||||
const StoredMessage &sm = messageStore.addFromPacket(p);
|
const StoredMessage &sm = messageStore.addFromPacket(p);
|
||||||
graphics::MessageRenderer::handleNewMessage(nullptr, sm, p); // notify UI
|
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
|
// Send the packet into the mesh
|
||||||
DEBUG_HEAP_BEFORE;
|
DEBUG_HEAP_BEFORE;
|
||||||
auto a = packetPool.allocCopy(p);
|
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");
|
LOG_DEBUG("Skip position ping; no fresh position since boot");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
LOG_INFO("Send position ping to 0x%08x, wantReplies=%d, channel=%d", dest, wantReplies, node->channel);
|
// Prefer the node's current channel, but fall back to the first channel with
|
||||||
positionModule->sendOurPosition(dest, wantReplies, node->channel);
|
// 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;
|
return true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -159,6 +159,12 @@ class MeshService
|
|||||||
*/
|
*/
|
||||||
void handleToRadio(meshtastic_MeshPacket &p);
|
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
|
/** 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
|
* @return true if client devices should be sent a new set of radio configs
|
||||||
*/
|
*/
|
||||||
|
|||||||
+53
-63
@@ -13,7 +13,6 @@
|
|||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "PacketHistory.h"
|
#include "PacketHistory.h"
|
||||||
#include "PowerFSM.h"
|
#include "PowerFSM.h"
|
||||||
#include "RTC.h"
|
|
||||||
#include "RadioInterface.h"
|
#include "RadioInterface.h"
|
||||||
#include "Router.h"
|
#include "Router.h"
|
||||||
#include "SPILock.h"
|
#include "SPILock.h"
|
||||||
@@ -21,12 +20,14 @@
|
|||||||
#include "TransmitHistory.h"
|
#include "TransmitHistory.h"
|
||||||
#include "TypeConversions.h"
|
#include "TypeConversions.h"
|
||||||
#include "error.h"
|
#include "error.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "memory/MemAudit.h"
|
#include "memory/MemAudit.h"
|
||||||
#include "mesh-pb-constants.h"
|
#include "mesh-pb-constants.h"
|
||||||
#include "mesh/generated/meshtastic/deviceonly_legacy.pb.h"
|
#include "mesh/generated/meshtastic/deviceonly_legacy.pb.h"
|
||||||
#include "meshUtils.h"
|
#include "meshUtils.h"
|
||||||
#include "modules/NeighborInfoModule.h"
|
#include "modules/NeighborInfoModule.h"
|
||||||
|
#include "target_specific.h"
|
||||||
#if HAS_VARIABLE_HOPS
|
#if HAS_VARIABLE_HOPS
|
||||||
#include "modules/HopScalingModule.h"
|
#include "modules/HopScalingModule.h"
|
||||||
#endif
|
#endif
|
||||||
@@ -50,11 +51,7 @@
|
|||||||
#include "SPILock.h"
|
#include "SPILock.h"
|
||||||
#include "modules/StoreForwardModule.h"
|
#include "modules/StoreForwardModule.h"
|
||||||
#include <Preferences.h>
|
#include <Preferences.h>
|
||||||
#include <esp_efuse.h>
|
|
||||||
#include <esp_efuse_table.h>
|
|
||||||
#include <nvs_flash.h>
|
#include <nvs_flash.h>
|
||||||
#include <soc/efuse_reg.h>
|
|
||||||
#include <soc/soc.h>
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef ARCH_PORTDUINO
|
#ifdef ARCH_PORTDUINO
|
||||||
@@ -372,8 +369,7 @@ void NodeDB::disarmNodeDatabaseDecodeTargets()
|
|||||||
*/
|
*/
|
||||||
uint32_t radioGeneration;
|
uint32_t radioGeneration;
|
||||||
|
|
||||||
// FIXME - move this somewhere else
|
// getMacAddr() and getDeviceId() are the per-architecture hooks declared in target_specific.h.
|
||||||
extern void getMacAddr(uint8_t *dmac);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -410,53 +406,13 @@ NodeDB::NodeDB()
|
|||||||
uint32_t channelFileCRC = crc32Buffer(&channelFile, sizeof(channelFile));
|
uint32_t channelFileCRC = crc32Buffer(&channelFile, sizeof(channelFile));
|
||||||
|
|
||||||
int saveWhat = 0;
|
int saveWhat = 0;
|
||||||
// Get device unique id
|
// Re-read the device id from silicon each boot via the per-arch getDeviceId(); clear the
|
||||||
#if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6)
|
// disk-loaded value first so a failed/empty derivation leaves it unset rather than stale.
|
||||||
uint32_t unique_id[4];
|
myNodeInfo.device_id.size = 0;
|
||||||
// ESP32 factory burns a unique id in efuse for S2+ series and evidently C3+ series
|
memset(myNodeInfo.device_id.bytes, 0, sizeof(myNodeInfo.device_id.bytes));
|
||||||
// This is used for HMACs in the esp-rainmaker AIOT platform and seems to be a good choice for us
|
if (getDeviceId(myNodeInfo.device_id.bytes)) {
|
||||||
esp_err_t err = esp_efuse_read_field_blob(ESP_EFUSE_OPTIONAL_UNIQUE_ID, unique_id, sizeof(unique_id) * 8);
|
myNodeInfo.device_id.size = sizeof(myNodeInfo.device_id.bytes);
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
#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
|
// 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
|
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);
|
LOG_DEBUG("Number of Device Reboots: %d", myNodeInfo.reboot_count);
|
||||||
#endif
|
#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
|
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);
|
// 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;
|
moduleConfig.telemetry.device_update_interval = MAX_INTERVAL;
|
||||||
#endif
|
#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
|
#ifdef USERPREFS_CONFIG_ENV_TELEM_UPDATE_INTERVAL
|
||||||
moduleConfig.telemetry.environment_update_interval = USERPREFS_CONFIG_ENV_TELEM_UPDATE_INTERVAL;
|
moduleConfig.telemetry.environment_update_interval = USERPREFS_CONFIG_ENV_TELEM_UPDATE_INTERVAL;
|
||||||
#else
|
#else
|
||||||
moduleConfig.telemetry.environment_update_interval = 0;
|
moduleConfig.telemetry.environment_update_interval = 0;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USERPREFS_CONFIG_ENVIRONMENT_MEASUREMENT_ENABLED
|
#ifdef USERPREFS_CONFIG_ENV_SCREEN_SCREEN_ENABLED
|
||||||
moduleConfig.telemetry.environment_measurement_enabled = USERPREFS_CONFIG_ENVIRONMENT_MEASUREMENT_ENABLED;
|
moduleConfig.telemetry.environment_screen_enabled = USERPREFS_CONFIG_ENV_SCREEN_SCREEN_ENABLED;
|
||||||
#endif
|
#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;
|
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.power_update_interval = 0;
|
||||||
moduleConfig.telemetry.health_update_interval = 0;
|
moduleConfig.telemetry.health_update_interval = 0;
|
||||||
moduleConfig.neighbor_info.update_interval = 0;
|
moduleConfig.neighbor_info.update_interval = 0;
|
||||||
@@ -1635,10 +1615,16 @@ void NodeDB::removeNodeByNum(NodeNum nodeNum)
|
|||||||
removed++;
|
removed++;
|
||||||
}
|
}
|
||||||
numMeshNodes -= removed;
|
numMeshNodes -= removed;
|
||||||
std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.begin() + numMeshNodes + 1,
|
if (removed) {
|
||||||
meshtastic_NodeInfoLite());
|
// Clear exactly the slots compaction vacated. Sizing this from `removed` (rather than a
|
||||||
if (removed)
|
// fixed one) keeps it inside the vector when nothing matched and the store is full.
|
||||||
eraseNodeSatellites(nodeNum);
|
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
|
#if WARM_NODE_COUNT > 0
|
||||||
// Explicit user removal: don't let the warm tier resurrect the node
|
// Explicit user removal: don't let the warm tier resurrect the node
|
||||||
warmStore.remove(nodeNum);
|
warmStore.remove(nodeNum);
|
||||||
@@ -1896,7 +1882,8 @@ void NodeDB::cleanupMeshDB()
|
|||||||
// Keep any key we learned (e.g. via a DM before the NodeInfo
|
// Keep any key we learned (e.g. via a DM before the NodeInfo
|
||||||
// exchange completed) rather than losing it with the purge.
|
// exchange completed) rather than losing it with the purge.
|
||||||
if (n.public_key.size == 32)
|
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
|
#endif
|
||||||
|
|
||||||
eraseNodeSatellites(gone);
|
eraseNodeSatellites(gone);
|
||||||
@@ -2080,7 +2067,7 @@ void NodeDB::demoteOldestHotNodesToWarm()
|
|||||||
// Keep the public key if we have one (40 B warm record); keyless nodes
|
// Keep the public key if we have one (40 B warm record); keyless nodes
|
||||||
// still get a placeholder so re-admission restores last_heard.
|
// 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,
|
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
|
// Demotion drops the node from the header table, so drop its satellites
|
||||||
// too (the eviction chokepoint) - they'd otherwise orphan until the next
|
// too (the eviction chokepoint) - they'd otherwise orphan until the next
|
||||||
// enforceSatelliteCaps pass.
|
// enforceSatelliteCaps pass.
|
||||||
@@ -3772,7 +3759,7 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
|
|||||||
// Demote to the warm tier so the identity (and crucially the
|
// Demote to the warm tier so the identity (and crucially the
|
||||||
// PKI key) outlives the hot-store slot.
|
// PKI key) outlives the hot-store slot.
|
||||||
warmStore.absorb(evicted.num, evicted.last_heard, evicted.public_key.size == 32 ? evicted.public_key.bytes : NULL,
|
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
|
#endif
|
||||||
eraseNodeSatellites(evicted.num);
|
eraseNodeSatellites(evicted.num);
|
||||||
// Shove the remaining nodes down the chain
|
// Shove the remaining nodes down the chain
|
||||||
@@ -3801,10 +3788,13 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
|
|||||||
// Re-admission: restore what the warm tier kept for this node
|
// Re-admission: restore what the warm tier kept for this node
|
||||||
WarmNodeEntry warm;
|
WarmNodeEntry warm;
|
||||||
if (warmStore.take(n, 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
|
// Restore the role the warm tier cached, so re-admission isn't stuck at CLIENT
|
||||||
// until the next NodeInfo arrives.
|
// until the next NodeInfo arrives.
|
||||||
lite->role = static_cast<meshtastic_Config_DeviceConfig_Role>(warmRoleOf(warm));
|
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))) {
|
if (!memfll(warm.public_key, 0, sizeof(warm.public_key))) {
|
||||||
lite->public_key.size = 32;
|
lite->public_key.size = 32;
|
||||||
memcpy(lite->public_key.bytes, warm.public_key, 32);
|
memcpy(lite->public_key.bytes, warm.public_key, 32);
|
||||||
|
|||||||
@@ -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"
|
#include "mqtt/MQTT.h"
|
||||||
#endif
|
#endif
|
||||||
#include "Throttle.h"
|
#include "Throttle.h"
|
||||||
#include <RTC.h>
|
#include "gps/RTC.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
@@ -1201,19 +1201,49 @@ void PhoneAPI::prefetchNodeInfos()
|
|||||||
onNowHasData(0);
|
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)
|
meshtastic_MeshPacket PhoneAPI::makeReplayPositionPacket(NodeNum num, const meshtastic_PositionLite &pos)
|
||||||
{
|
{
|
||||||
// Shape this exactly like a fresh live broadcast Position from the peer so the
|
// Shape this exactly like a fresh live broadcast Position from the peer so the
|
||||||
// phone runs it through its normal "live position broadcast" handler path.
|
// 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.
|
// to=ourNum would read as a DM-from-peer and never lands in node detail UI.
|
||||||
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default;
|
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default;
|
||||||
|
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
||||||
pkt.from = num;
|
pkt.from = num;
|
||||||
pkt.to = NODENUM_BROADCAST;
|
pkt.to = NODENUM_BROADCAST;
|
||||||
pkt.id = generatePacketId();
|
|
||||||
pkt.rx_time = pos.time;
|
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.channel = 0;
|
||||||
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
pkt.rx_snr = header ? header->snr : 0;
|
||||||
pkt.hop_start = pkt.hop_limit;
|
setReplayHopFields(pkt, header);
|
||||||
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||||
// Mark as if heard over the air, not internally generated
|
// Mark as if heard over the air, not internally generated
|
||||||
pkt.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
|
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;
|
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default;
|
||||||
pkt.from = num;
|
pkt.from = num;
|
||||||
pkt.to = NODENUM_BROADCAST;
|
pkt.to = NODENUM_BROADCAST;
|
||||||
pkt.id = generatePacketId();
|
|
||||||
// No native timestamp on telemetry packets here; use last_heard.
|
// No native timestamp on telemetry packets here; use last_heard.
|
||||||
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
||||||
pkt.rx_time = header ? header->last_heard : 0;
|
pkt.rx_time = header ? header->last_heard : 0;
|
||||||
|
pkt.id = makeReplayPacketId(num, pkt.rx_time, meshtastic_Telemetry_device_metrics_tag);
|
||||||
pkt.channel = 0;
|
pkt.channel = 0;
|
||||||
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
pkt.rx_snr = header ? header->snr : 0;
|
||||||
pkt.hop_start = pkt.hop_limit;
|
setReplayHopFields(pkt, header);
|
||||||
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||||
// Mark as if heard over the air, not internally generated - iOS client filters
|
// Mark as if heard over the air, not internally generated - iOS client filters
|
||||||
// TRANSPORT_INTERNAL packets out of broadcast peer state updates.
|
// 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;
|
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default;
|
||||||
pkt.from = num;
|
pkt.from = num;
|
||||||
pkt.to = NODENUM_BROADCAST;
|
pkt.to = NODENUM_BROADCAST;
|
||||||
pkt.id = generatePacketId();
|
|
||||||
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
||||||
pkt.rx_time = header ? header->last_heard : 0;
|
pkt.rx_time = header ? header->last_heard : 0;
|
||||||
|
pkt.id = makeReplayPacketId(num, pkt.rx_time, meshtastic_Telemetry_environment_metrics_tag);
|
||||||
pkt.channel = 0;
|
pkt.channel = 0;
|
||||||
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
pkt.rx_snr = header ? header->snr : 0;
|
||||||
pkt.hop_start = pkt.hop_limit;
|
setReplayHopFields(pkt, header);
|
||||||
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||||
// Mark as if heard over the air, not internally generated - iOS client filters
|
// Mark as if heard over the air, not internally generated - iOS client filters
|
||||||
// TRANSPORT_INTERNAL packets out of broadcast peer state updates.
|
// 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;
|
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default;
|
||||||
pkt.from = num;
|
pkt.from = num;
|
||||||
pkt.to = NODENUM_BROADCAST;
|
pkt.to = NODENUM_BROADCAST;
|
||||||
pkt.id = generatePacketId();
|
|
||||||
// StatusMessage has no native timestamp; use last_heard.
|
// StatusMessage has no native timestamp; use last_heard.
|
||||||
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
||||||
pkt.rx_time = header ? header->last_heard : 0;
|
pkt.rx_time = header ? header->last_heard : 0;
|
||||||
|
pkt.id = makeReplayPacketId(num, pkt.rx_time, meshtastic_PortNum_NODE_STATUS_APP);
|
||||||
pkt.channel = 0;
|
pkt.channel = 0;
|
||||||
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
pkt.rx_snr = header ? header->snr : 0;
|
||||||
pkt.hop_start = pkt.hop_limit;
|
setReplayHopFields(pkt, header);
|
||||||
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||||
// Mark as if heard over the air, not internally generated - client filters
|
// Mark as if heard over the air, not internally generated - client filters
|
||||||
pkt.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
|
pkt.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
|
||||||
@@ -1667,6 +1697,19 @@ bool PhoneAPI::wasSeenRecently(uint32_t id)
|
|||||||
return false;
|
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
|
* 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
|
// the gate here closes that race and covers H6/H7 from the
|
||||||
// audit: get_config_request and set_config from unauthed
|
// audit: get_config_request and set_config from unauthed
|
||||||
// clients no longer reach AdminModule at all.
|
// clients no longer reach AdminModule at all.
|
||||||
if (p.from == 0 && p.which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
|
// Gate on the connection, not the wire `from`, which a client can forge to a non-zero value to
|
||||||
p.decoded.portnum == meshtastic_PortNum_ADMIN_APP) {
|
// 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;
|
meshtastic_AdminMessage admin = meshtastic_AdminMessage_init_zero;
|
||||||
if (pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_AdminMessage_msg, &admin)) {
|
switch (classifyLocalAdminPacket(p, getAdminAuthorized(), admin)) {
|
||||||
if (admin.which_payload_variant == meshtastic_AdminMessage_lockdown_auth_tag) {
|
case LocalAdminGate::LockdownAuth: {
|
||||||
handleLockdownAuthInline(admin.lockdown_auth);
|
handleLockdownAuthInline(admin.lockdown_auth);
|
||||||
// Wipe the decoded passphrase scratch - the byte array in
|
// The encoded wipe is the security-critical one: nothing else clears the passphrase from
|
||||||
// p.decoded.payload.bytes is wiped by handleLockdownAuthInline.
|
// the packet buffer. handleLockdownAuthInline already zeroes the decoded copy up to its
|
||||||
volatile uint8_t *adminVol = const_cast<volatile uint8_t *>(admin.lockdown_auth.passphrase.bytes);
|
// size; re-zero the full scratch capacity here as defense in depth.
|
||||||
for (size_t i = 0; i < sizeof(admin.lockdown_auth.passphrase.bytes); i++)
|
volatile uint8_t *adminVol = const_cast<volatile uint8_t *>(admin.lockdown_auth.passphrase.bytes);
|
||||||
adminVol[i] = 0;
|
for (size_t i = 0; i < sizeof(admin.lockdown_auth.passphrase.bytes); i++)
|
||||||
return true;
|
adminVol[i] = 0;
|
||||||
}
|
volatile uint8_t *encodedVol = const_cast<volatile uint8_t *>(p.decoded.payload.bytes);
|
||||||
if (!getAdminAuthorized()) {
|
for (size_t i = 0; i < sizeof(p.decoded.payload.bytes); i++)
|
||||||
LOG_WARN("Lockdown: dropping admin payload variant=%d from unauthorized connection", admin.which_payload_variant);
|
encodedVol[i] = 0;
|
||||||
return false;
|
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
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -319,4 +319,18 @@ class PhoneAPI
|
|||||||
|
|
||||||
/// If the mesh service tells us fromNum has changed, tell the phone
|
/// If the mesh service tells us fromNum has changed, tell the phone
|
||||||
virtual int onNotify(uint32_t newValue) override;
|
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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -184,11 +184,9 @@ const RegionInfo regions[] = {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
433,05-434,7 Mhz 10 mW
|
433,05-434,7 Mhz 10 mW
|
||||||
868,0-868,6 Mhz 25 mW
|
https://zakon.rada.gov.ua/laws/show/262-2026-п
|
||||||
https://nkrzi.gov.ua/images/upload/256/5810/PDF_UUZ_19_01_2016.pdf
|
|
||||||
*/
|
*/
|
||||||
RDEF(UA_433, 433.0f, 434.7f, 10, 10, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
RDEF(UA_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
|
Malaysia
|
||||||
|
|||||||
+73
-36
@@ -5,7 +5,7 @@
|
|||||||
#include "MeshService.h"
|
#include "MeshService.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "PositionPrecision.h"
|
#include "PositionPrecision.h"
|
||||||
#include "RTC.h"
|
#include "gps/RTC.h"
|
||||||
|
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
@@ -452,7 +452,7 @@ void Router::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Rout
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA)
|
#if !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA)
|
||||||
bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p, size_t encodedDataSize)
|
bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p)
|
||||||
{
|
{
|
||||||
// Only a signature we verify below may mark this packet signed; never trust an inbound flag.
|
// Only a signature we verify below may mark this packet signed; never trust an inbound flag.
|
||||||
p->xeddsa_signed = false;
|
p->xeddsa_signed = false;
|
||||||
@@ -483,17 +483,17 @@ bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p, size_t encodedDataSize)
|
|||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
// Truly unsigned (signature size 0) - only reject the class a signing node always signs: a
|
// Truly unsigned (signature size 0) - only reject the class a signing node always signs: a
|
||||||
// non-PKI broadcast whose signed encoding would still fit the LoRa frame. encodedDataSize is
|
// non-PKI broadcast whose signed encoding would still fit the LoRa frame. Size p->decoded
|
||||||
// the size of the encoded Data exactly as the sender built it (or 0 to size p->decoded
|
// canonically so this counts the same fields the sender's signedDataFits() gate counted;
|
||||||
// canonically); with no signature field present it is the unsigned base, and adding
|
// adding XEDDSA_SIGNATURE_FIELD_BYTES to that unsigned base mirrors it exactly, whatever
|
||||||
// XEDDSA_SIGNATURE_FIELD_BYTES mirrors the sender-side signedDataFits() gate per packet,
|
// fields the Data carried. Unicast/PKI packets and broadcasts too big to carry a signature
|
||||||
// whatever fields the Data carried. Unicast/PKI packets and broadcasts too big to carry a
|
// are never signed, so they must not be hard-failed here even for a known signer.
|
||||||
// signature are never signed, so they must not be hard-failed here even for a known signer.
|
|
||||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(p->from);
|
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(p->from);
|
||||||
if (node && nodeInfoLiteHasXeddsaSigned(node) && !p->pki_encrypted && isBroadcast(p->to)) {
|
if (node && nodeInfoLiteHasXeddsaSigned(node) && !p->pki_encrypted && isBroadcast(p->to)) {
|
||||||
if (encodedDataSize == 0 && !pb_get_encoded_size(&encodedDataSize, &meshtastic_Data_msg, &p->decoded))
|
size_t canonicalSize;
|
||||||
|
if (!pb_get_encoded_size(&canonicalSize, &meshtastic_Data_msg, &p->decoded))
|
||||||
return true; // can't size it; never drop on a sizing failure
|
return true; // can't size it; never drop on a sizing failure
|
||||||
if (encodedDataSize + XEDDSA_SIGNATURE_FIELD_BYTES + MESHTASTIC_HEADER_LENGTH <= MAX_LORA_PAYLOAD_LEN) {
|
if (canonicalSize + XEDDSA_SIGNATURE_FIELD_BYTES + MESHTASTIC_HEADER_LENGTH <= MAX_LORA_PAYLOAD_LEN) {
|
||||||
LOG_WARN("Dropping unsigned broadcast from 0x%08x that previously signed", p->from);
|
LOG_WARN("Dropping unsigned broadcast from 0x%08x that previously signed", p->from);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -524,36 +524,60 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
|||||||
bool decrypted = false;
|
bool decrypted = false;
|
||||||
ChannelIndex chIndex = 0;
|
ChannelIndex chIndex = 0;
|
||||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||||
// Attempt PKI decryption first. The sender's key may come from the hot
|
// Resolve the sender's public key: prefer the one stored in NodeDB (hot store or warm tier), else
|
||||||
// store or the warm tier (nodes evicted from the hot store keep their key
|
// fall back to a not-yet-committed key held during an in-progress key-verification handshake.
|
||||||
// there), so DMs from long-tail nodes still decrypt.
|
meshtastic_NodeInfoLite_public_key_t remotePublic = {0, {0}};
|
||||||
meshtastic_NodeInfoLite_public_key_t fromKey = {0, {0}};
|
bool haveRemoteKey = nodeDB->copyPublicKey(p->from, remotePublic) || crypto->getPendingPublicKey(p->from, remotePublic);
|
||||||
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) {
|
|
||||||
LOG_DEBUG("Attempt PKI decryption");
|
|
||||||
|
|
||||||
if (crypto->decryptCurve25519(p->from, fromKey, p->id, rawSize, p->encrypted.bytes, bytes)) {
|
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) {
|
||||||
|
// 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, 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!");
|
LOG_INFO("PKI Decryption worked!");
|
||||||
|
|
||||||
meshtastic_Data decodedtmp;
|
meshtastic_Data decodedtmp;
|
||||||
memset(&decodedtmp, 0, sizeof(decodedtmp));
|
memset(&decodedtmp, 0, sizeof(decodedtmp));
|
||||||
rawSize -= MESHTASTIC_PKC_OVERHEAD;
|
size_t payloadSize = rawSize - MESHTASTIC_PKC_OVERHEAD;
|
||||||
if (pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp) &&
|
if (pb_decode_from_bytes(bytes, payloadSize, &meshtastic_Data_msg, &decodedtmp) &&
|
||||||
decodedtmp.portnum != meshtastic_PortNum_UNKNOWN_APP) {
|
decodedtmp.portnum != meshtastic_PortNum_UNKNOWN_APP) {
|
||||||
decrypted = true;
|
decrypted = true;
|
||||||
|
rawSize = payloadSize; // commit the overhead subtraction only on full success
|
||||||
LOG_INFO("Packet decrypted using PKI!");
|
LOG_INFO("Packet decrypted using PKI!");
|
||||||
p->pki_encrypted = true;
|
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->public_key.size = 32;
|
||||||
p->decoded = decodedtmp;
|
p->decoded = decodedtmp;
|
||||||
p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded
|
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 {
|
} else {
|
||||||
|
// AEAD already authenticated this ciphertext, so no other candidate could decode it -
|
||||||
|
// the payload is simply malformed.
|
||||||
LOG_ERROR("PKC Decrypted, but pb_decode failed!");
|
LOG_ERROR("PKC Decrypted, but pb_decode failed!");
|
||||||
return DecodeState::DECODE_FAILURE;
|
return DecodeState::DECODE_FAILURE;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
LOG_WARN("PKC decrypt attempted but failed!");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -597,17 +621,17 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
|||||||
if (decrypted) {
|
if (decrypted) {
|
||||||
// parsing was successful
|
// parsing was successful
|
||||||
p->channel = chIndex; // change to store the index instead of the hash
|
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)
|
#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
|
// Runs before the bitfield merge below: that merge can set want_response, adding wire bytes
|
||||||
// MESHTASTIC_PKC_OVERHEAD subtraction preserves that, and PKI packets are unicast so the
|
// the sender never encoded and skewing the policy's sizing of p->decoded.
|
||||||
// downgrade predicate ignores them anyway).
|
if (!checkXeddsaReceivePolicy(p))
|
||||||
if (!checkXeddsaReceivePolicy(p, rawSize))
|
|
||||||
return DecodeState::DECODE_FAILURE;
|
return DecodeState::DECODE_FAILURE;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
if (p->decoded.has_bitfield)
|
||||||
|
p->decoded.want_response |= p->decoded.bitfield & BITFIELD_WANT_RESPONSE_MASK;
|
||||||
|
|
||||||
/* Not actually ever used.
|
/* Not actually ever used.
|
||||||
// Decompress if needed. jm
|
// Decompress if needed. jm
|
||||||
if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP) {
|
if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP) {
|
||||||
@@ -729,7 +753,7 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
|
|||||||
|
|
||||||
LOG_DEBUG("Original length - %d ", p->decoded.payload.size);
|
LOG_DEBUG("Original length - %d ", p->decoded.payload.size);
|
||||||
LOG_DEBUG("Compressed length - %d ", compressed_len);
|
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 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) {
|
if (compressed_len >= p->decoded.payload.size) {
|
||||||
@@ -758,10 +782,17 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
|
|||||||
ChannelIndex chIndex = p->channel; // keep as a local because we are about to change it
|
ChannelIndex chIndex = p->channel; // keep as a local because we are about to change it
|
||||||
|
|
||||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||||
// Destination key from the hot store or the warm tier (evicted
|
// Resolve the destination's public key: prefer NodeDB (hot store or warm tier - evicted
|
||||||
// long-tail nodes keep their key there)
|
// 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}};
|
meshtastic_NodeInfoLite_public_key_t destKey = {0, {0}};
|
||||||
bool haveDestKey = nodeDB->copyPublicKey(p->to, destKey);
|
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
|
// 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
|
// is not in the local nodedb
|
||||||
// First, only PKC encrypt packets we are originating
|
// First, only PKC encrypt packets we are originating
|
||||||
@@ -779,11 +810,17 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
|
|||||||
config.security.private_key.size == 32 && !isBroadcast(p->to) &&
|
config.security.private_key.size == 32 && !isBroadcast(p->to) &&
|
||||||
// Some portnums either make no sense to send with PKC
|
// 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_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!");
|
LOG_DEBUG("Use PKI!");
|
||||||
if (numbytes + MESHTASTIC_HEADER_LENGTH + MESHTASTIC_PKC_OVERHEAD > MAX_LORA_PAYLOAD_LEN)
|
if (numbytes + MESHTASTIC_HEADER_LENGTH + MESHTASTIC_PKC_OVERHEAD > MAX_LORA_PAYLOAD_LEN)
|
||||||
return meshtastic_Routing_Error_TOO_LARGE;
|
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) {
|
if (!haveDestKey) {
|
||||||
LOG_WARN("Unknown public key for destination node 0x%08x (portnum %d), refusing to send legacy DM", p->to,
|
LOG_WARN("Unknown public key for destination node 0x%08x (portnum %d), refusing to send legacy DM", p->to,
|
||||||
p->decoded.portnum);
|
p->decoded.portnum);
|
||||||
|
|||||||
+3
-4
@@ -183,16 +183,15 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p);
|
|||||||
* downgrade protection: drop a non-PKI broadcast from a known signer whose signed encoding would
|
* downgrade protection: drop a non-PKI broadcast from a known signer whose signed encoding would
|
||||||
* still fit a LoRa frame (unicast, PKI, and oversized broadcasts always pass).
|
* still fit a LoRa frame (unicast, PKI, and oversized broadcasts always pass).
|
||||||
*
|
*
|
||||||
* encodedDataSize is the wire size of the encoded Data as the sender built it; pass 0 to size
|
* The fit test sizes p->decoded with the real encoder, so it measures the fields the sender
|
||||||
* p->decoded canonically instead (for already-decoded ingress such as plaintext-MQTT downlink,
|
* encoded rather than any raw wire length.
|
||||||
* which bypasses perhapsDecode's crypto path).
|
|
||||||
*
|
*
|
||||||
* The caller MUST hold cryptLock: verification runs through the shared CryptoEngine key cache.
|
* The caller MUST hold cryptLock: verification runs through the shared CryptoEngine key cache.
|
||||||
* (perhapsDecode already holds it; other call sites must take it themselves.)
|
* (perhapsDecode already holds it; other call sites must take it themselves.)
|
||||||
*
|
*
|
||||||
* @return false if the packet must be dropped.
|
* @return false if the packet must be dropped.
|
||||||
*/
|
*/
|
||||||
bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p, size_t encodedDataSize = 0);
|
bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
extern Router *router;
|
extern Router *router;
|
||||||
|
|||||||
@@ -173,30 +173,6 @@ template <typename T> bool SX126xInterface<T>::init()
|
|||||||
LOG_WARN("Failed to apply SX1262 register 0x8B5 patch for RX improvement");
|
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)
|
if (res == RADIOLIB_ERR_NONE)
|
||||||
res = lora.setCRC(RADIOLIB_SX126X_LORA_CRC_ON);
|
res = lora.setCRC(RADIOLIB_SX126X_LORA_CRC_ON);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
#include "StreamAPI.h"
|
#include "StreamAPI.h"
|
||||||
#include "PowerFSM.h"
|
#include "PowerFSM.h"
|
||||||
#include "RTC.h"
|
|
||||||
#include "Throttle.h"
|
#include "Throttle.h"
|
||||||
#include "concurrency/LockGuard.h"
|
#include "concurrency/LockGuard.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
|
|
||||||
#define START1 0x94
|
#define START1 0x94
|
||||||
#define START2 0xc3
|
#define START2 0xc3
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#include "TransmitHistory.h"
|
#include "TransmitHistory.h"
|
||||||
#include "FSCommon.h"
|
#include "FSCommon.h"
|
||||||
#include "RTC.h"
|
|
||||||
#include "SPILock.h"
|
#include "SPILock.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
#include <Throttle.h>
|
#include <Throttle.h>
|
||||||
|
|
||||||
#ifdef FSCom
|
#ifdef FSCom
|
||||||
|
|||||||
+57
-39
@@ -13,12 +13,11 @@
|
|||||||
|
|
||||||
#if defined(NRF52840_XXAA)
|
#if defined(NRF52840_XXAA)
|
||||||
#include "flash/flash_nrf5x.h"
|
#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.
|
#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
|
// Older pages are still read on upgrade: v1 keeps identity + key but discards last_heard,
|
||||||
// DISCARD its last_heard (the old timestamp would be misread as role/protected bits).
|
// v2 keeps the word but clears bit 6, which was still part of the timestamp then.
|
||||||
// Records re-rank and re-learn their role on the next contact. Legacy pages convert to
|
|
||||||
// v2 naturally as the ring rotates.
|
|
||||||
// A tombstone is an entry record whose last_heard is all-ones - getTime()
|
// A tombstone is an entry record whose last_heard is all-ones - getTime()
|
||||||
// (unix seconds) cannot reach 0xFFFFFFFF until 2106, and erased flash is
|
// (unix seconds) cannot reach 0xFFFFFFFF until 2106, and erased flash is
|
||||||
// detected via num == 0xFFFFFFFF before last_heard is ever inspected.
|
// 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");
|
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 \
|
#define WARM_STORE_MAGIC_V1 \
|
||||||
0x314D5257u // "WRM1" - v1: last_heard was a plain timestamp. On upgrade we keep
|
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
|
#ifdef FSCom
|
||||||
static const char *warmFileName = "/prefs/warm.dat";
|
static const char *warmFileName = "/prefs/warm.dat";
|
||||||
@@ -134,18 +136,18 @@ WarmNodeEntry *WarmNodeStore::place(NodeNum num, uint32_t lastHeard, const uint8
|
|||||||
return slot;
|
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
|
// Pack role + protected category + signer into the low bits of last_heard. place() and
|
||||||
// replay store the raw word verbatim, so the metadata round-trips through flash.
|
// ring replay store the raw word verbatim, so the metadata round-trips through flash.
|
||||||
const uint32_t packed = warmPackLastHeard(lastHeard, role, protectedCat);
|
const uint32_t packed = warmPackLastHeard(lastHeard, role, protectedCat, signer);
|
||||||
const WarmNodeEntry *slot = place(num, packed, key32);
|
const WarmNodeEntry *slot = place(num, packed, key32);
|
||||||
if (!slot)
|
if (!slot)
|
||||||
return false;
|
return false;
|
||||||
persistEntry(*slot);
|
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,
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,19 +260,24 @@ bool WarmNodeStore::saveIfDirty()
|
|||||||
// (stranded live entries re-appended, then erased). Flash access holds spiLock -
|
// (stranded live entries re-appended, then erased). Flash access holds spiLock -
|
||||||
// the page cache is shared with InternalFS/LittleFS.
|
// 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));
|
flash_nrf5x_read(&h, WARM_FLASH_PAGE_ADDR(page), sizeof(h));
|
||||||
if (h.seq == 0xFFFFFFFFu)
|
if (h.seq == 0xFFFFFFFFu)
|
||||||
return false; // erased page
|
return false; // erased page
|
||||||
if (h.magic == WARM_RING_MAGIC) {
|
if (h.magic == WARM_RING_MAGIC) {
|
||||||
if (legacy)
|
if (fmt)
|
||||||
*legacy = false;
|
*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;
|
return true;
|
||||||
}
|
}
|
||||||
if (h.magic == WARM_RING_MAGIC_V1) {
|
if (h.magic == WARM_RING_MAGIC_V1) {
|
||||||
if (legacy)
|
if (fmt)
|
||||||
*legacy = true; // v1 page: replay it, but discard last_heard (see WARM_RING_MAGIC_V1)
|
*fmt = WarmFormat::V1; // replay it, but discard last_heard (see WARM_RING_MAGIC_V1)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -386,13 +393,13 @@ void WarmNodeStore::load()
|
|||||||
// Order valid pages by ascending seq so replay applies oldest first
|
// Order valid pages by ascending seq so replay applies oldest first
|
||||||
uint8_t order[WARM_FLASH_PAGES] = {};
|
uint8_t order[WARM_FLASH_PAGES] = {};
|
||||||
uint32_t seqs[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 nValid = 0;
|
||||||
uint8_t nCorrupt = 0;
|
uint8_t nCorrupt = 0;
|
||||||
for (uint8_t p = 0; p < WARM_FLASH_PAGES; p++) {
|
for (uint8_t p = 0; p < WARM_FLASH_PAGES; p++) {
|
||||||
WarmPageHeader h;
|
WarmPageHeader h;
|
||||||
bool legacy = false;
|
WarmFormat fmt = WarmFormat::Current;
|
||||||
if (!ringReadHeader(p, h, &legacy)) {
|
if (!ringReadHeader(p, h, &fmt)) {
|
||||||
// An erased page reads back all-ones; any other magic is a
|
// An erased page reads back all-ones; any other magic is a
|
||||||
// partially-written or bit-rotted header we're dropping, so flag it
|
// partially-written or bit-rotted header we're dropping, so flag it
|
||||||
// rather than silently treating the loss as a clean empty ring.
|
// rather than silently treating the loss as a clean empty ring.
|
||||||
@@ -400,7 +407,7 @@ void WarmNodeStore::load()
|
|||||||
nCorrupt++;
|
nCorrupt++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
legacyOf[p] = legacy;
|
fmtOf[p] = fmt;
|
||||||
uint8_t pos = nValid;
|
uint8_t pos = nValid;
|
||||||
while (pos > 0 && static_cast<int32_t>(h.seq - seqs[pos - 1]) < 0) {
|
while (pos > 0 && static_cast<int32_t>(h.seq - seqs[pos - 1]) < 0) {
|
||||||
order[pos] = order[pos - 1];
|
order[pos] = order[pos - 1];
|
||||||
@@ -427,7 +434,7 @@ void WarmNodeStore::load()
|
|||||||
uint32_t migrated = 0;
|
uint32_t migrated = 0;
|
||||||
for (uint8_t k = 0; k < nValid; k++) {
|
for (uint8_t k = 0; k < nValid; k++) {
|
||||||
const uint8_t p = order[k];
|
const uint8_t p = order[k];
|
||||||
const bool legacy = legacyOf[p];
|
const WarmFormat fmt = fmtOf[p];
|
||||||
uint16_t slot = 0;
|
uint16_t slot = 0;
|
||||||
for (; slot < kRecordsPerPage; slot++) {
|
for (; slot < kRecordsPerPage; slot++) {
|
||||||
WarmNodeEntry rec;
|
WarmNodeEntry rec;
|
||||||
@@ -444,12 +451,15 @@ void WarmNodeStore::load()
|
|||||||
memset(e, 0, sizeof(*e));
|
memset(e, 0, sizeof(*e));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// v1 (legacy) record: keep identity + key, but discard the old timestamp -
|
// Normalise older records: v1's timestamp would be misread as role/protected,
|
||||||
// its low bits would otherwise be misread as role/protected metadata.
|
// and v2's bit 6 as a signer we never verified.
|
||||||
uint32_t lh = rec.last_heard;
|
uint32_t lh = rec.last_heard;
|
||||||
if (legacy) {
|
if (fmt == WarmFormat::V1) {
|
||||||
lh = 0;
|
lh = 0;
|
||||||
migrated++;
|
migrated++;
|
||||||
|
} else if (fmt == WarmFormat::V2) {
|
||||||
|
lh &= ~(WARM_SIGNER_MASK << WARM_SIGNER_SHIFT);
|
||||||
|
migrated++;
|
||||||
}
|
}
|
||||||
const WarmNodeEntry *e = place(rec.num, lh, rec.public_key);
|
const WarmNodeEntry *e = place(rec.num, lh, rec.public_key);
|
||||||
if (e)
|
if (e)
|
||||||
@@ -460,17 +470,16 @@ void WarmNodeStore::load()
|
|||||||
activePage = p;
|
activePage = p;
|
||||||
writeSlot = slot;
|
writeSlot = slot;
|
||||||
nextSeq = seqs[k] + 1;
|
nextSeq = seqs[k] + 1;
|
||||||
// If the head is a v1 page, force the next append to rotate into a fresh v2 page,
|
// Rotate rather than append into an older-format page: a later load would
|
||||||
// so new (v2) records never land in a page whose header says v1 (which would make
|
// normalise the new records to that page's format and drop metadata.
|
||||||
// a later load discard their last_heard - including the role/protected we just set).
|
if (fmt != WarmFormat::Current)
|
||||||
if (legacy)
|
|
||||||
writeSlot = kRecordsPerPage;
|
writeSlot = kRecordsPerPage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (nCorrupt)
|
if (nCorrupt)
|
||||||
LOG_WARN("WarmStore: dropped %u corrupt ring page(s), some nodes lost", nCorrupt);
|
LOG_WARN("WarmStore: dropped %u corrupt ring page(s), some nodes lost", nCorrupt);
|
||||||
if (migrated)
|
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(),
|
LOG_INFO("WarmStore: replayed %u ring records -> %u live nodes (page %u, slot %u)", (unsigned)replayed, (unsigned)count(),
|
||||||
activePage, writeSlot);
|
activePage, writeSlot);
|
||||||
}
|
}
|
||||||
@@ -537,10 +546,14 @@ void WarmNodeStore::load()
|
|||||||
LOG_WARN("WarmStore: %s header read failed, starting empty", warmFileName);
|
LOG_WARN("WarmStore: %s header read failed, starting empty", warmFileName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// v1 (WRM1) is still accepted: same record size, but its last_heard was a plain
|
// Older snapshots are still accepted: same record size, fewer metadata bits in
|
||||||
// timestamp. We keep identity + key and discard last_heard on load (see below).
|
// last_heard. Both are normalised to the current format below.
|
||||||
const bool legacy = (h.magic == WARM_STORE_MAGIC_V1);
|
const bool known = h.magic == WARM_STORE_MAGIC || h.magic == WARM_STORE_MAGIC_V2 || h.magic == WARM_STORE_MAGIC_V1;
|
||||||
if ((h.magic != WARM_STORE_MAGIC && !legacy) || h.entrySize != sizeof(WarmNodeEntry) || h.count > WARM_NODE_COUNT) {
|
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();
|
f.close();
|
||||||
LOG_WARN("WarmStore: %s header invalid (magic=0x%08x entrySize=%u count=%u), starting empty", warmFileName, h.magic,
|
LOG_WARN("WarmStore: %s header invalid (magic=0x%08x entrySize=%u count=%u), starting empty", warmFileName, h.magic,
|
||||||
h.entrySize, h.count);
|
h.entrySize, h.count);
|
||||||
@@ -560,19 +573,24 @@ void WarmNodeStore::load()
|
|||||||
memset(entries, 0, WARM_NODE_COUNT * sizeof(WarmNodeEntry));
|
memset(entries, 0, WARM_NODE_COUNT * sizeof(WarmNodeEntry));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (legacy) {
|
// Normalise older records, then mark dirty so save() rewrites in the current format:
|
||||||
// Migrate v1 → v2: discard the old last_heard (its low bits would be misread as
|
// v1's timestamp would be misread as role/protected, v2's bit 6 as an unverified signer.
|
||||||
// role/protected); keep num + public_key. Mark dirty so save() rewrites as v2.
|
if (fmt == WarmFormat::V1) {
|
||||||
for (size_t i = 0; i < WARM_NODE_COUNT; i++)
|
for (size_t i = 0; i < WARM_NODE_COUNT; i++)
|
||||||
if (entries[i].num)
|
if (entries[i].num)
|
||||||
entries[i].last_heard = 0;
|
entries[i].last_heard = 0;
|
||||||
dirty = true;
|
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 {
|
} else {
|
||||||
f.close();
|
f.close();
|
||||||
}
|
}
|
||||||
LOG_INFO("WarmStore: loaded %u warm nodes from %s%s", h.count, warmFileName,
|
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()
|
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
|
// 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
|
// 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
|
// the evicted node's device role, a protected category + a signer flag, at zero cost to
|
||||||
// (entry stays 40 B; no RAM/flash growth). The high bits remain a real unix-seconds
|
// record size (entry stays 40 B; no RAM/flash growth). The high bits remain a real
|
||||||
// timestamp quantised to (1 << WARM_META_BITS) seconds.
|
// unix-seconds timestamp quantised to (1 << WARM_META_BITS) seconds.
|
||||||
//
|
//
|
||||||
// Safe because: a real timestamp can never be all-ones (the tombstone sentinel) before
|
// 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
|
// 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.
|
// 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_BITS = 7; // role(4) + protected(2) + signer(1)
|
||||||
static constexpr uint32_t WARM_META_MASK = (1u << WARM_META_BITS) - 1; // 0x3F → 64 s quantum
|
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; // 0xFFFFFFC0
|
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_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_SHIFT = 4; // bits [5:4] protected category
|
||||||
static constexpr uint32_t WARM_PROT_MASK = 0x03u;
|
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.
|
// Protected category cached alongside role so consumers needn't re-derive the mapping.
|
||||||
enum class WarmProtected : uint8_t { None = 0, Role = 1, Flag = 2 };
|
enum class WarmProtected : uint8_t { None = 0, Role = 1, Flag = 2 };
|
||||||
|
|
||||||
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) |
|
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)
|
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);
|
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
|
// Gated on NRF52840_XXAA: the ring sits at 0xEA000
|
||||||
// valid only on the 1 MB-flash nRF52840.
|
// valid only on the 1 MB-flash nRF52840.
|
||||||
@@ -99,9 +109,11 @@ class WarmNodeStore
|
|||||||
/// entries; otherwise the oldest (keyless-first) entry is replaced.
|
/// entries; otherwise the oldest (keyless-first) entry is replaced.
|
||||||
/// @param role the node's device role (meshtastic_Config_DeviceConfig_Role, 0..12)
|
/// @param role the node's device role (meshtastic_Config_DeviceConfig_Role, 0..12)
|
||||||
/// @param protectedCat WarmProtected category cached for the hop-trim path
|
/// @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
|
/// @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,
|
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.
|
/// Look up the cached device role + protected category for a warm node.
|
||||||
/// @return false if the node is not in the warm tier.
|
/// @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 ringAppend(const WarmNodeEntry &rec, int storeSlot /* -1 for tombstones */);
|
||||||
void ringRotate(); // reclaim oldest page, compacting stranded live entries
|
void ringRotate(); // reclaim oldest page, compacting stranded live entries
|
||||||
void ringOpenPage(uint8_t page); // erase + write header (seq = nextSeq++)
|
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
|
#endif
|
||||||
|
|
||||||
bool save();
|
bool save();
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#include "mesh/eth/ethClient.h"
|
#include "mesh/eth/ethClient.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "RTC.h"
|
|
||||||
#include "concurrency/Periodic.h"
|
#include "concurrency/Periodic.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "mesh/api/ethServerAPI.h"
|
#include "mesh/api/ethServerAPI.h"
|
||||||
#include "target_specific.h"
|
#include "target_specific.h"
|
||||||
|
|||||||
@@ -6,10 +6,34 @@
|
|||||||
#error Regenerate this file with the current version of nanopb generator.
|
#error Regenerate this file with the current version of nanopb generator.
|
||||||
#endif
|
#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
|
#endif
|
||||||
|
|
||||||
/* Enum definitions */
|
/* Enum definitions */
|
||||||
typedef enum _meshtastic_MessageType {
|
/* Version of the interdevice protocol spoken on the link. Both sides send
|
||||||
meshtastic_MessageType_ACK = 0,
|
theirs in the ping/pong handshake; a peer reporting a different one runs
|
||||||
meshtastic_MessageType_COLLECT_INTERVAL = 160, /* in ms */
|
firmware that does not match and is not talked to.
|
||||||
meshtastic_MessageType_BEEP_ON = 161, /* duration ms */
|
|
||||||
meshtastic_MessageType_BEEP_OFF = 162, /* cancel prematurely */
|
On a change that breaks the other side (renumbered fields, changed
|
||||||
meshtastic_MessageType_SHUTDOWN = 163,
|
semantics, removed messages), raise the value of CURRENT. Do not add
|
||||||
meshtastic_MessageType_POWER_ON = 164,
|
another entry: this enum carries a single constant, not a history. */
|
||||||
meshtastic_MessageType_SCD41_TEMP = 176,
|
typedef enum _meshtastic_InterdeviceVersion {
|
||||||
meshtastic_MessageType_SCD41_HUMIDITY = 177,
|
meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_UNSPECIFIED = 0,
|
||||||
meshtastic_MessageType_SCD41_CO2 = 178,
|
/* Never use 1: ping/pong were bools before the handshake existed, and a
|
||||||
meshtastic_MessageType_AHT20_TEMP = 179,
|
bool true is the same varint on the wire as the number 1, so firmware
|
||||||
meshtastic_MessageType_AHT20_HUMIDITY = 180,
|
predating the handshake would pass it. */
|
||||||
meshtastic_MessageType_TVOC_INDEX = 181
|
meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_CURRENT = 2
|
||||||
} meshtastic_MessageType;
|
} 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 */
|
/* Struct definitions */
|
||||||
typedef struct _meshtastic_SensorData {
|
typedef PB_BYTES_ARRAY_T(4096) meshtastic_FileTransfer_filedata_t;
|
||||||
/* The message type */
|
/* Message for file operations */
|
||||||
meshtastic_MessageType type;
|
typedef struct _meshtastic_FileTransfer {
|
||||||
pb_size_t which_data;
|
meshtastic_FileOperation operation; /* File operation (GET, POST, PUT, DELETE) */
|
||||||
union {
|
char filepath[256]; /* Path of the file on the SD card */
|
||||||
float float_value;
|
meshtastic_FileTransfer_filedata_t filedata; /* Chunk content (POST/PUT request, GET response) */
|
||||||
uint32_t uint32_value;
|
meshtastic_FileStatus status; /* Response: outcome of the operation */
|
||||||
} data;
|
char message[255]; /* Response: human readable detail, may be empty */
|
||||||
} meshtastic_SensorData;
|
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 {
|
typedef struct _meshtastic_InterdeviceMessage {
|
||||||
pb_size_t which_data;
|
pb_size_t which_data;
|
||||||
union {
|
union {
|
||||||
char nmea[1024];
|
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;
|
} 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;
|
} meshtastic_InterdeviceMessage;
|
||||||
|
|
||||||
|
|
||||||
@@ -50,53 +206,205 @@ extern "C" {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
/* Helper constants for enums */
|
/* Helper constants for enums */
|
||||||
#define _meshtastic_MessageType_MIN meshtastic_MessageType_ACK
|
#define _meshtastic_InterdeviceVersion_MIN meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_UNSPECIFIED
|
||||||
#define _meshtastic_MessageType_MAX meshtastic_MessageType_TVOC_INDEX
|
#define _meshtastic_InterdeviceVersion_MAX meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_CURRENT
|
||||||
#define _meshtastic_MessageType_ARRAYSIZE ((meshtastic_MessageType)(meshtastic_MessageType_TVOC_INDEX+1))
|
#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 */
|
/* Initializer values for message structs */
|
||||||
#define meshtastic_SensorData_init_default {_meshtastic_MessageType_MIN, 0, {0}}
|
#define meshtastic_FileTransfer_init_default {_meshtastic_FileOperation_MIN, "", {0, {0}}, _meshtastic_FileStatus_MIN, "", 0, 0, 0}
|
||||||
#define meshtastic_InterdeviceMessage_init_default {0, {""}}
|
#define meshtastic_DirectoryListing_init_default {"", 0, {"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""}, _meshtastic_FileStatus_MIN, "", 0, 0}
|
||||||
#define meshtastic_SensorData_init_zero {_meshtastic_MessageType_MIN, 0, {0}}
|
#define meshtastic_I2CTransaction_init_default {0, {0, {0}}, 0}
|
||||||
#define meshtastic_InterdeviceMessage_init_zero {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) */
|
/* Field tags (for use in manual encoding/decoding) */
|
||||||
#define meshtastic_SensorData_type_tag 1
|
#define meshtastic_FileTransfer_operation_tag 1
|
||||||
#define meshtastic_SensorData_float_value_tag 2
|
#define meshtastic_FileTransfer_filepath_tag 2
|
||||||
#define meshtastic_SensorData_uint32_value_tag 3
|
#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_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 */
|
/* Struct field encoding specification for nanopb */
|
||||||
#define meshtastic_SensorData_FIELDLIST(X, a) \
|
#define meshtastic_FileTransfer_FIELDLIST(X, a) \
|
||||||
X(a, STATIC, SINGULAR, UENUM, type, 1) \
|
X(a, STATIC, SINGULAR, UENUM, operation, 1) \
|
||||||
X(a, STATIC, ONEOF, FLOAT, (data,float_value,data.float_value), 2) \
|
X(a, STATIC, SINGULAR, STRING, filepath, 2) \
|
||||||
X(a, STATIC, ONEOF, UINT32, (data,uint32_value,data.uint32_value), 3)
|
X(a, STATIC, SINGULAR, BYTES, filedata, 3) \
|
||||||
#define meshtastic_SensorData_CALLBACK NULL
|
X(a, STATIC, SINGULAR, UENUM, status, 4) \
|
||||||
#define meshtastic_SensorData_DEFAULT NULL
|
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) \
|
#define meshtastic_InterdeviceMessage_FIELDLIST(X, a) \
|
||||||
X(a, STATIC, ONEOF, STRING, (data,nmea,data.nmea), 1) \
|
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_CALLBACK NULL
|
||||||
#define meshtastic_InterdeviceMessage_DEFAULT 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;
|
extern const pb_msgdesc_t meshtastic_InterdeviceMessage_msg;
|
||||||
|
|
||||||
/* Defines for backwards compatibility with code written before nanopb-0.4.0 */
|
/* 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
|
#define meshtastic_InterdeviceMessage_fields &meshtastic_InterdeviceMessage_msg
|
||||||
|
|
||||||
/* Maximum encoded size of messages (where known) */
|
/* Maximum encoded size of messages (where known) */
|
||||||
#define MESHTASTIC_MESHTASTIC_INTERDEVICE_PB_H_MAX_SIZE meshtastic_InterdeviceMessage_size
|
#define MESHTASTIC_MESHTASTIC_INTERDEVICE_PB_H_MAX_SIZE meshtastic_InterdeviceMessage_size
|
||||||
#define meshtastic_InterdeviceMessage_size 1026
|
#define meshtastic_DirectoryListing_size 4657
|
||||||
#define meshtastic_SensorData_size 9
|
#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
|
#ifdef __cplusplus
|
||||||
} /* extern "C" */
|
} /* extern "C" */
|
||||||
|
|||||||
@@ -121,7 +121,9 @@ typedef enum _meshtastic_TelemetrySensorType {
|
|||||||
/* ICM-42607-P 6‑Axis IMU */
|
/* ICM-42607-P 6‑Axis IMU */
|
||||||
meshtastic_TelemetrySensorType_ICM42607P = 53,
|
meshtastic_TelemetrySensorType_ICM42607P = 53,
|
||||||
/* SPA06 pressure and temperature */
|
/* SPA06 pressure and temperature */
|
||||||
meshtastic_TelemetrySensorType_SPA06 = 54
|
meshtastic_TelemetrySensorType_SPA06 = 54,
|
||||||
|
/* HM330X PM SENSOR */
|
||||||
|
meshtastic_TelemetrySensorType_HM330X = 55
|
||||||
} meshtastic_TelemetrySensorType;
|
} meshtastic_TelemetrySensorType;
|
||||||
|
|
||||||
/* Struct definitions */
|
/* Struct definitions */
|
||||||
@@ -502,8 +504,8 @@ extern "C" {
|
|||||||
|
|
||||||
/* Helper constants for enums */
|
/* Helper constants for enums */
|
||||||
#define _meshtastic_TelemetrySensorType_MIN meshtastic_TelemetrySensorType_SENSOR_UNSET
|
#define _meshtastic_TelemetrySensorType_MIN meshtastic_TelemetrySensorType_SENSOR_UNSET
|
||||||
#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_SPA06
|
#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_HM330X
|
||||||
#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_SPA06+1))
|
#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_HM330X+1))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -96,14 +96,6 @@ static void taskCreateCert(void *parameter)
|
|||||||
{
|
{
|
||||||
prefs.begin("MeshtasticHTTPS", false);
|
prefs.begin("MeshtasticHTTPS", false);
|
||||||
|
|
||||||
#if 0
|
|
||||||
// Delete the saved certs (used in debugging)
|
|
||||||
LOG_DEBUG("Delete any saved SSL keys");
|
|
||||||
// prefs.clear();
|
|
||||||
prefs.remove("PK");
|
|
||||||
prefs.remove("cert");
|
|
||||||
#endif
|
|
||||||
|
|
||||||
LOG_INFO("Checking if we have a saved SSL Certificate");
|
LOG_INFO("Checking if we have a saved SSL Certificate");
|
||||||
|
|
||||||
size_t pkLen = prefs.getBytesLength("PK");
|
size_t pkLen = prefs.getBytesLength("PK");
|
||||||
|
|||||||
@@ -231,8 +231,6 @@ int callback_static_file(const struct _u_request *request, struct _u_response *r
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void handleWebResponse() {}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Adapt the radioapi to the Webservice handleAPIv1ToRadio
|
* Adapt the radioapi to the Webservice handleAPIv1ToRadio
|
||||||
* Trigger : WebGui(SAVE)->WebServcice->phoneApi
|
* Trigger : WebGui(SAVE)->WebServcice->phoneApi
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#if HAS_WIFI
|
#if HAS_WIFI
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "RTC.h"
|
|
||||||
#include "concurrency/Periodic.h"
|
#include "concurrency/Periodic.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
#include "mesh/wifi/WiFiAPClient.h"
|
#include "mesh/wifi/WiFiAPClient.h"
|
||||||
|
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "meshUtils.h"
|
#include "meshUtils.h"
|
||||||
|
#include "target_specific.h"
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -79,6 +80,20 @@ bool memfll(const uint8_t *mem, uint8_t find, size_t numbytes)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool getMacAddrDeviceId(uint8_t *deviceId)
|
||||||
|
{
|
||||||
|
// Zero-initialized: getMacAddr() may return without writing (e.g. Portduino with no MAC
|
||||||
|
// source), and we want that no-write case to hit the all-zeros guard below, not read garbage.
|
||||||
|
uint8_t mac[6] = {0};
|
||||||
|
getMacAddr(mac);
|
||||||
|
if (memfll(mac, 0, sizeof(mac))) {
|
||||||
|
LOG_WARN("MAC is all zeros, leaving device_id unset");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
memcpy(deviceId, mac, sizeof(mac));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool isOneOf(int item, int count, ...)
|
bool isOneOf(int item, int count, ...)
|
||||||
{
|
{
|
||||||
va_list args;
|
va_list args;
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ void printBytes(const char *label, const uint8_t *p, size_t numbytes);
|
|||||||
// is the memory region filled with a single character?
|
// is the memory region filled with a single character?
|
||||||
bool memfll(const uint8_t *mem, uint8_t find, size_t numbytes);
|
bool memfll(const uint8_t *mem, uint8_t find, size_t numbytes);
|
||||||
|
|
||||||
|
// getDeviceId() fallback (see target_specific.h): copies the 6-byte factory MAC, or returns false
|
||||||
|
// on an all-zero MAC so two blank devices don't collide on an all-zero id.
|
||||||
|
bool getMacAddrDeviceId(uint8_t *deviceId);
|
||||||
|
|
||||||
bool isOneOf(int item, int count, ...);
|
bool isOneOf(int item, int count, ...);
|
||||||
|
|
||||||
const std::string vformat(const char *const zcFormat, ...);
|
const std::string vformat(const char *const zcFormat, ...);
|
||||||
|
|||||||
+164
-23
@@ -1,15 +1,17 @@
|
|||||||
#include "AdminModule.h"
|
#include "AdminModule.h"
|
||||||
#include "Channels.h"
|
#include "Channels.h"
|
||||||
#include "DisplayFormatters.h"
|
#include "DisplayFormatters.h"
|
||||||
|
#include "HardwareRNG.h"
|
||||||
#include "MeshService.h"
|
#include "MeshService.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "PositionPrecision.h"
|
#include "PositionPrecision.h"
|
||||||
#include "PowerFSM.h"
|
#include "PowerFSM.h"
|
||||||
#include "RTC.h"
|
|
||||||
#include "SPILock.h"
|
#include "SPILock.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
#include "input/InputBroker.h"
|
#include "input/InputBroker.h"
|
||||||
#include "meshUtils.h"
|
#include "meshUtils.h"
|
||||||
#include <FSCommon.h>
|
#include <FSCommon.h>
|
||||||
|
#include <Throttle.h>
|
||||||
#include <ctype.h> // for better whitespace handling
|
#include <ctype.h> // for better whitespace handling
|
||||||
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI
|
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI
|
||||||
#include "MeshtasticOTA.h"
|
#include "MeshtasticOTA.h"
|
||||||
@@ -48,6 +50,9 @@
|
|||||||
#include "GPS.h"
|
#include "GPS.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include <RNG.h> // CryptRNG, the seeded CSPRNG used as fallback for the session passkey
|
||||||
|
#include <Throttle.h> // rollover-safe elapsed-time checks for the session passkey
|
||||||
|
|
||||||
#if MESHTASTIC_EXCLUDE_GPS
|
#if MESHTASTIC_EXCLUDE_GPS
|
||||||
#include "modules/PositionModule.h"
|
#include "modules/PositionModule.h"
|
||||||
#endif
|
#endif
|
||||||
@@ -125,9 +130,16 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
meshtastic_Channel *ch = &channels.getByIndex(mp.channel);
|
meshtastic_Channel *ch = &channels.getByIndex(mp.channel);
|
||||||
// Could tighten this up further by tracking the last public_key we went an AdminMessage request to
|
|
||||||
// and only allowing responses from that remote.
|
|
||||||
if (messageIsResponse(r)) {
|
if (messageIsResponse(r)) {
|
||||||
|
// Only accept a response from a remote we sent the matching request to. from == 0 is a
|
||||||
|
// local client, which PhoneAPI has already gated.
|
||||||
|
const pb_size_t moduleConfigTag = r->which_payload_variant == meshtastic_AdminMessage_get_module_config_response_tag
|
||||||
|
? r->get_module_config_response.which_payload_variant
|
||||||
|
: 0;
|
||||||
|
if (mp.from != 0 && !responseIsSolicited(mp, r->which_payload_variant, moduleConfigTag)) {
|
||||||
|
LOG_INFO("Ignore admin response from 0x%08x, no outstanding request", mp.from);
|
||||||
|
return handled;
|
||||||
|
}
|
||||||
LOG_DEBUG("Allow admin response message");
|
LOG_DEBUG("Allow admin response message");
|
||||||
} else if (mp.from == 0) {
|
} else if (mp.from == 0) {
|
||||||
// Local admin from a BLE/USB/TCP client. from == 0 cannot arrive from the
|
// Local admin from a BLE/USB/TCP client. from == 0 cannot arrive from the
|
||||||
@@ -472,8 +484,9 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
|||||||
}
|
}
|
||||||
case meshtastic_AdminMessage_get_module_config_response_tag: {
|
case meshtastic_AdminMessage_get_module_config_response_tag: {
|
||||||
LOG_INFO("Client received a get_module_config response");
|
LOG_INFO("Client received a get_module_config response");
|
||||||
if (fromOthers && r->get_module_config_response.which_payload_variant ==
|
// which_payload_variant is the ModuleConfig oneof tag, so compare against that tag, not the
|
||||||
meshtastic_AdminMessage_ModuleConfigType_REMOTEHARDWARE_CONFIG) {
|
// AdminMessage ModuleConfigType enum (whose REMOTEHARDWARE value is a different number).
|
||||||
|
if (fromOthers && r->get_module_config_response.which_payload_variant == meshtastic_ModuleConfig_remote_hardware_tag) {
|
||||||
handleGetModuleConfigResponse(mp, r);
|
handleGetModuleConfigResponse(mp, r);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -773,6 +786,27 @@ void AdminModule::handleSetOwner(const meshtastic_User &o)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && \
|
||||||
|
!MESHTASTIC_EXCLUDE_ACCELEROMETER
|
||||||
|
// Shared by double-tap-as-button (device) and wake-on-motion (display). Start on either flag's
|
||||||
|
// off->on edge; stop on the on->off edge once neither needs it (disable() clears `enabled` so a
|
||||||
|
// later re-enable restarts). Skip the stop if the sensor also drives the compass, else the heading
|
||||||
|
// freezes until reboot. wasOn/nowOn = old/new of the changed flag; otherFeatureOn = the other flag.
|
||||||
|
static void reconcileAccelerometerThread(bool wasOn, bool nowOn, bool otherFeatureOn)
|
||||||
|
{
|
||||||
|
if (!accelerometerThread) // null unless a sensor was detected at boot
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!wasOn && nowOn && accelerometerThread->enabled == false) {
|
||||||
|
accelerometerThread->enabled = true;
|
||||||
|
accelerometerThread->start();
|
||||||
|
} else if (wasOn && !nowOn && !otherFeatureOn && accelerometerThread->enabled == true &&
|
||||||
|
!accelerometerThread->providesHeading()) {
|
||||||
|
accelerometerThread->disable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers)
|
void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers)
|
||||||
{
|
{
|
||||||
auto changes = SEGMENT_CONFIG;
|
auto changes = SEGMENT_CONFIG;
|
||||||
@@ -788,12 +822,8 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers)
|
|||||||
config.has_device = true;
|
config.has_device = true;
|
||||||
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && \
|
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && \
|
||||||
!MESHTASTIC_EXCLUDE_ACCELEROMETER
|
!MESHTASTIC_EXCLUDE_ACCELEROMETER
|
||||||
if (config.device.double_tap_as_button_press == false && c.payload_variant.device.double_tap_as_button_press == true &&
|
reconcileAccelerometerThread(config.device.double_tap_as_button_press,
|
||||||
accelerometerThread->enabled == false) {
|
c.payload_variant.device.double_tap_as_button_press, config.display.wake_on_tap_or_motion);
|
||||||
config.device.double_tap_as_button_press = c.payload_variant.device.double_tap_as_button_press;
|
|
||||||
accelerometerThread->enabled = true;
|
|
||||||
accelerometerThread->start();
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
if (config.device.button_gpio == c.payload_variant.device.button_gpio &&
|
if (config.device.button_gpio == c.payload_variant.device.button_gpio &&
|
||||||
config.device.buzzer_gpio == c.payload_variant.device.buzzer_gpio &&
|
config.device.buzzer_gpio == c.payload_variant.device.buzzer_gpio &&
|
||||||
@@ -892,12 +922,8 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers)
|
|||||||
}
|
}
|
||||||
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && \
|
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && \
|
||||||
!MESHTASTIC_EXCLUDE_ACCELEROMETER
|
!MESHTASTIC_EXCLUDE_ACCELEROMETER
|
||||||
if (config.display.wake_on_tap_or_motion == false && c.payload_variant.display.wake_on_tap_or_motion == true &&
|
reconcileAccelerometerThread(config.display.wake_on_tap_or_motion, c.payload_variant.display.wake_on_tap_or_motion,
|
||||||
accelerometerThread->enabled == false) {
|
config.device.double_tap_as_button_press);
|
||||||
config.display.wake_on_tap_or_motion = c.payload_variant.display.wake_on_tap_or_motion;
|
|
||||||
accelerometerThread->enabled = true;
|
|
||||||
accelerometerThread->start();
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
config.display = c.payload_variant.display;
|
config.display = c.payload_variant.display;
|
||||||
break;
|
break;
|
||||||
@@ -1408,6 +1434,15 @@ void AdminModule::handleGetConfig(const meshtastic_MeshPacket &req, const uint32
|
|||||||
LOG_INFO("Get config: Security");
|
LOG_INFO("Get config: Security");
|
||||||
res.get_config_response.which_payload_variant = meshtastic_Config_security_tag;
|
res.get_config_response.which_payload_variant = meshtastic_Config_security_tag;
|
||||||
res.get_config_response.payload_variant.security = config.security;
|
res.get_config_response.payload_variant.security = config.security;
|
||||||
|
// The device identity private key is backup material for the local owner only. A local
|
||||||
|
// admin client sets from == 0 (BLE/USB/TCP); never return the key to a remote requester,
|
||||||
|
// even an authorized one, since it would travel over the air. public_key/admin_key are
|
||||||
|
// public and stay put.
|
||||||
|
if (req.from != 0) {
|
||||||
|
auto &sec = res.get_config_response.payload_variant.security;
|
||||||
|
memset(sec.private_key.bytes, 0, sizeof(sec.private_key.bytes));
|
||||||
|
sec.private_key.size = 0;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case meshtastic_AdminMessage_ConfigType_SESSIONKEY_CONFIG:
|
case meshtastic_AdminMessage_ConfigType_SESSIONKEY_CONFIG:
|
||||||
LOG_INFO("Get config: Sessionkey");
|
LOG_INFO("Get config: Sessionkey");
|
||||||
@@ -1774,11 +1809,15 @@ AdminModule::AdminModule() : ProtobufModule("Admin", meshtastic_PortNum_ADMIN_AP
|
|||||||
|
|
||||||
void AdminModule::setPassKey(meshtastic_AdminMessage *res)
|
void AdminModule::setPassKey(meshtastic_AdminMessage *res)
|
||||||
{
|
{
|
||||||
if (session_time == 0 || millis() / 1000 > session_time + 150) {
|
// Regenerate once there is no session yet or the current key is older than 150s. session_time
|
||||||
for (int i = 0; i < 8; i++) {
|
// holds millis(); the Throttle check is rollover-safe, unlike the previous seconds comparison.
|
||||||
session_passkey[i] = random();
|
if (!sessionPasskeyValid || !Throttle::isWithinTimespanMs(session_time, 150 * 1000UL)) {
|
||||||
}
|
// Session passkey authenticates admin replies, so it must be unpredictable: prefer the
|
||||||
session_time = millis() / 1000;
|
// hardware RNG, falling back to the seeded CSPRNG only when no hardware source exists.
|
||||||
|
if (!HardwareRNG::fill(session_passkey, sizeof(session_passkey)))
|
||||||
|
CryptRNG.rand(session_passkey, sizeof(session_passkey));
|
||||||
|
session_time = millis();
|
||||||
|
sessionPasskeyValid = true;
|
||||||
}
|
}
|
||||||
memcpy(res->session_passkey.bytes, session_passkey, 8);
|
memcpy(res->session_passkey.bytes, session_passkey, 8);
|
||||||
res->session_passkey.size = 8;
|
res->session_passkey.size = 8;
|
||||||
@@ -1791,7 +1830,8 @@ bool AdminModule::checkPassKey(meshtastic_AdminMessage *res)
|
|||||||
{ // check that the key in the packet is still valid
|
{ // check that the key in the packet is still valid
|
||||||
printBytes("Incoming session key: ", res->session_passkey.bytes, 8);
|
printBytes("Incoming session key: ", res->session_passkey.bytes, 8);
|
||||||
printBytes("Expected session key: ", session_passkey, 8);
|
printBytes("Expected session key: ", session_passkey, 8);
|
||||||
return (session_time + 300 > millis() / 1000 && res->session_passkey.size == 8 &&
|
// Key is valid for 300s from issue; sessionPasskeyValid guards an unissued session.
|
||||||
|
return (sessionPasskeyValid && Throttle::isWithinTimespanMs(session_time, 300 * 1000UL) && res->session_passkey.size == 8 &&
|
||||||
memcmp(res->session_passkey.bytes, session_passkey, 8) == 0);
|
memcmp(res->session_passkey.bytes, session_passkey, 8) == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1812,6 +1852,107 @@ bool AdminModule::messageIsResponse(const meshtastic_AdminMessage *r)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The response variant that answers a getter request, or 0 if the request has none.
|
||||||
|
static pb_size_t adminResponseForRequest(pb_size_t requestVariant)
|
||||||
|
{
|
||||||
|
switch (requestVariant) {
|
||||||
|
case meshtastic_AdminMessage_get_channel_request_tag:
|
||||||
|
return meshtastic_AdminMessage_get_channel_response_tag;
|
||||||
|
case meshtastic_AdminMessage_get_owner_request_tag:
|
||||||
|
return meshtastic_AdminMessage_get_owner_response_tag;
|
||||||
|
case meshtastic_AdminMessage_get_config_request_tag:
|
||||||
|
return meshtastic_AdminMessage_get_config_response_tag;
|
||||||
|
case meshtastic_AdminMessage_get_module_config_request_tag:
|
||||||
|
return meshtastic_AdminMessage_get_module_config_response_tag;
|
||||||
|
case meshtastic_AdminMessage_get_canned_message_module_messages_request_tag:
|
||||||
|
return meshtastic_AdminMessage_get_canned_message_module_messages_response_tag;
|
||||||
|
case meshtastic_AdminMessage_get_device_metadata_request_tag:
|
||||||
|
return meshtastic_AdminMessage_get_device_metadata_response_tag;
|
||||||
|
case meshtastic_AdminMessage_get_ringtone_request_tag:
|
||||||
|
return meshtastic_AdminMessage_get_ringtone_response_tag;
|
||||||
|
case meshtastic_AdminMessage_get_device_connection_status_request_tag:
|
||||||
|
return meshtastic_AdminMessage_get_device_connection_status_response_tag;
|
||||||
|
case meshtastic_AdminMessage_get_node_remote_hardware_pins_request_tag:
|
||||||
|
return meshtastic_AdminMessage_get_node_remote_hardware_pins_response_tag;
|
||||||
|
case meshtastic_AdminMessage_get_ui_config_request_tag:
|
||||||
|
return meshtastic_AdminMessage_get_ui_config_response_tag;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AdminModule::noteOutgoingAdminRequest(const meshtastic_MeshPacket &p)
|
||||||
|
{
|
||||||
|
if (p.which_payload_variant != meshtastic_MeshPacket_decoded_tag || p.decoded.portnum != meshtastic_PortNum_ADMIN_APP)
|
||||||
|
return;
|
||||||
|
// Local admin is answered in-process, and a broadcast is never a request to one remote.
|
||||||
|
if (p.to == 0 || isBroadcast(p.to) || p.to == nodeDB->getNodeNum())
|
||||||
|
return;
|
||||||
|
|
||||||
|
meshtastic_AdminMessage admin = meshtastic_AdminMessage_init_zero;
|
||||||
|
if (!pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_AdminMessage_msg, &admin))
|
||||||
|
return;
|
||||||
|
const pb_size_t responseVariant = adminResponseForRequest(admin.which_payload_variant);
|
||||||
|
if (!responseVariant)
|
||||||
|
return; // not a getter whose response we can pair
|
||||||
|
|
||||||
|
const bool keyValid = p.pki_encrypted && p.public_key.size == 32;
|
||||||
|
|
||||||
|
// One entry per request (a client sends N indexed get_channel requests, each answered once, so
|
||||||
|
// entries must not merge). Free slot, else evict the oldest by rollover-safe elapsed time.
|
||||||
|
OutstandingAdminRequest *slot = nullptr;
|
||||||
|
for (auto &o : outstandingAdminRequests)
|
||||||
|
if (o.to == 0) {
|
||||||
|
slot = &o;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!slot) {
|
||||||
|
const uint32_t now = millis();
|
||||||
|
slot = &outstandingAdminRequests[0];
|
||||||
|
for (auto &o : outstandingAdminRequests)
|
||||||
|
if ((uint32_t)(now - o.sentAtMs) > (uint32_t)(now - slot->sentAtMs))
|
||||||
|
slot = &o;
|
||||||
|
}
|
||||||
|
|
||||||
|
slot->to = p.to;
|
||||||
|
slot->sentAtMs = millis();
|
||||||
|
slot->expectedResponse = responseVariant;
|
||||||
|
slot->moduleConfigType = admin.which_payload_variant == meshtastic_AdminMessage_get_module_config_request_tag
|
||||||
|
? (uint8_t)admin.get_module_config_request
|
||||||
|
: 0;
|
||||||
|
slot->keyValid = keyValid;
|
||||||
|
if (keyValid)
|
||||||
|
memcpy(slot->key, p.public_key.bytes, 32);
|
||||||
|
else
|
||||||
|
memset(slot->key, 0, 32);
|
||||||
|
LOG_DEBUG("Admin request sent to 0x%08x, expecting its response", p.to);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AdminModule::responseIsSolicited(const meshtastic_MeshPacket &mp, pb_size_t responseVariant, pb_size_t moduleConfigTag)
|
||||||
|
{
|
||||||
|
// Scan every entry: several requests for the same variant may differ only in pinning, and an
|
||||||
|
// unpinned one still authorizes the response even if a pinned one does not.
|
||||||
|
for (auto &o : outstandingAdminRequests) {
|
||||||
|
if (o.to != mp.from || o.expectedResponse != responseVariant)
|
||||||
|
continue;
|
||||||
|
if (!Throttle::isWithinTimespanMs(o.sentAtMs, kOutstandingAdminRequestMs)) {
|
||||||
|
o.to = 0; // lapsed; free the slot and keep looking for another live match
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// A request pinned to a PKC key must be answered over PKC by that same key.
|
||||||
|
if (o.keyValid && (!mp.pki_encrypted || mp.public_key.size != 32 || memcmp(mp.public_key.bytes, o.key, 32) != 0))
|
||||||
|
continue;
|
||||||
|
// remote_hardware is the only module config that mutates state (the pin table), so require
|
||||||
|
// it to answer a request for that exact subtype, not just any get_module_config_request.
|
||||||
|
if (moduleConfigTag == meshtastic_ModuleConfig_remote_hardware_tag &&
|
||||||
|
o.moduleConfigType != meshtastic_AdminMessage_ModuleConfigType_REMOTEHARDWARE_CONFIG)
|
||||||
|
continue;
|
||||||
|
o.to = 0; // consume: one request authorizes one response, no replay within the window
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool AdminModule::messageIsRequest(const meshtastic_AdminMessage *r)
|
bool AdminModule::messageIsRequest(const meshtastic_AdminMessage *r)
|
||||||
{
|
{
|
||||||
if (r->which_payload_variant == meshtastic_AdminMessage_get_channel_request_tag ||
|
if (r->which_payload_variant == meshtastic_AdminMessage_get_channel_request_tag ||
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ class AdminModule : public ProtobufModule<meshtastic_AdminMessage>, public Obser
|
|||||||
bool hasOpenEditTransaction = false;
|
bool hasOpenEditTransaction = false;
|
||||||
|
|
||||||
uint8_t session_passkey[8] = {0};
|
uint8_t session_passkey[8] = {0};
|
||||||
uint session_time = 0;
|
uint32_t session_time = 0; // millis() when the current session passkey was issued
|
||||||
|
bool sessionPasskeyValid = false; // separate flag: millis() 0 at boot is a valid issue time
|
||||||
|
|
||||||
void saveChanges(int saveWhat, bool shouldReboot = true);
|
void saveChanges(int saveWhat, bool shouldReboot = true);
|
||||||
|
|
||||||
@@ -77,7 +78,29 @@ class AdminModule : public ProtobufModule<meshtastic_AdminMessage>, public Obser
|
|||||||
public:
|
public:
|
||||||
void handleSetHamMode(const meshtastic_HamParameters &req);
|
void handleSetHamMode(const meshtastic_HamParameters &req);
|
||||||
|
|
||||||
|
/// Note an admin request leaving this node for a remote, so that remote's response is
|
||||||
|
/// accepted. Called from the client-to-mesh path (MeshService::handleToRadio).
|
||||||
|
void noteOutgoingAdminRequest(const meshtastic_MeshPacket &p);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// An admin response has no session passkey and its sender need not hold an admin key, so a
|
||||||
|
// request we sent is the only thing vouching for it. Track each request independently (its own
|
||||||
|
// expiry and pinned key) so a later one can't extend or relax an earlier one.
|
||||||
|
static constexpr size_t kOutstandingAdminRequests = 8;
|
||||||
|
static constexpr uint32_t kOutstandingAdminRequestMs = 300 * 1000; // same window as the session passkey
|
||||||
|
struct OutstandingAdminRequest {
|
||||||
|
NodeNum to; // 0 = free slot
|
||||||
|
uint32_t sentAtMs; // millis() when this request went out
|
||||||
|
pb_size_t expectedResponse; // the one response variant this request authorizes
|
||||||
|
uint8_t moduleConfigType; // for get_module_config_request: which ModuleConfigType we asked
|
||||||
|
uint8_t key[32]; // pinned destination key when the request went out over PKC
|
||||||
|
bool keyValid;
|
||||||
|
};
|
||||||
|
OutstandingAdminRequest outstandingAdminRequests[kOutstandingAdminRequests] = {};
|
||||||
|
|
||||||
|
/// Whether a response (variant responseVariant, module-config subtype moduleConfigTag or 0)
|
||||||
|
/// from mp.from answers a request we sent; consumes the matched request so it can't be replayed.
|
||||||
|
bool responseIsSolicited(const meshtastic_MeshPacket &mp, pb_size_t responseVariant, pb_size_t moduleConfigTag);
|
||||||
void handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uicfg);
|
void handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uicfg);
|
||||||
void handleSendInputEvent(const meshtastic_AdminMessage_InputEvent &inputEvent);
|
void handleSendInputEvent(const meshtastic_AdminMessage_InputEvent &inputEvent);
|
||||||
void reboot(int32_t seconds);
|
void reboot(int32_t seconds);
|
||||||
|
|||||||
@@ -289,10 +289,6 @@ void CannedMessageModule::updateDestinationSelectionList()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
meshtastic_MeshPacket *p = allocDataPacket();
|
|
||||||
p->pki_encrypted = true;
|
|
||||||
p->channel = 0;
|
|
||||||
|
|
||||||
// Populate active channels
|
// Populate active channels
|
||||||
std::vector<String> seenChannels;
|
std::vector<String> seenChannels;
|
||||||
seenChannels.reserve(channels.getNumChannels());
|
seenChannels.reserve(channels.getNumChannels());
|
||||||
|
|||||||
@@ -16,10 +16,10 @@
|
|||||||
#include "ExternalNotificationModule.h"
|
#include "ExternalNotificationModule.h"
|
||||||
#include "MeshService.h"
|
#include "MeshService.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "RTC.h"
|
|
||||||
#include "Router.h"
|
#include "Router.h"
|
||||||
#include "buzz/buzz.h"
|
#include "buzz/buzz.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "mesh/generated/meshtastic/rtttl.pb.h"
|
#include "mesh/generated/meshtastic/rtttl.pb.h"
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
#if !MESHTASTIC_EXCLUDE_PKI
|
#if !MESHTASTIC_EXCLUDE_PKI
|
||||||
#include "KeyVerificationModule.h"
|
#include "KeyVerificationModule.h"
|
||||||
|
#include "CryptoEngine.h"
|
||||||
|
#include "HardwareRNG.h"
|
||||||
#include "MeshService.h"
|
#include "MeshService.h"
|
||||||
#include "RTC.h"
|
#include "gps/RTC.h"
|
||||||
#include "graphics/draw/MenuHandler.h"
|
#include "graphics/draw/MenuHandler.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "meshUtils.h"
|
#include "meshUtils.h"
|
||||||
#include "modules/AdminModule.h"
|
#include "modules/AdminModule.h"
|
||||||
|
#include "modules/NodeInfoModule.h"
|
||||||
|
#include <RNG.h>
|
||||||
#include <SHA256.h>
|
#include <SHA256.h>
|
||||||
|
|
||||||
KeyVerificationModule *keyVerificationModule;
|
KeyVerificationModule *keyVerificationModule;
|
||||||
@@ -34,7 +38,7 @@ AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(cons
|
|||||||
{
|
{
|
||||||
updateState();
|
updateState();
|
||||||
if (request->which_payload_variant == meshtastic_AdminMessage_key_verification_tag && mp.from == 0) {
|
if (request->which_payload_variant == meshtastic_AdminMessage_key_verification_tag && mp.from == 0) {
|
||||||
LOG_WARN("Handling Key Verification Admin Message type %u", request->key_verification.message_type);
|
LOG_DEBUG("Handling Key Verification Admin Message type %u", request->key_verification.message_type);
|
||||||
|
|
||||||
if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_INITIATE_VERIFICATION &&
|
if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_INITIATE_VERIFICATION &&
|
||||||
currentState == KEY_VERIFICATION_IDLE) {
|
currentState == KEY_VERIFICATION_IDLE) {
|
||||||
@@ -48,9 +52,7 @@ AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(cons
|
|||||||
|
|
||||||
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_VERIFY &&
|
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_VERIFY &&
|
||||||
request->key_verification.nonce == currentNonce) {
|
request->key_verification.nonce == currentNonce) {
|
||||||
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
commitVerifiedRemoteNode();
|
||||||
if (remoteNodePtr)
|
|
||||||
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
|
||||||
resetToIdle();
|
resetToIdle();
|
||||||
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY) {
|
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY) {
|
||||||
resetToIdle();
|
resetToIdle();
|
||||||
@@ -63,9 +65,8 @@ AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(cons
|
|||||||
bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_KeyVerification *r)
|
bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_KeyVerification *r)
|
||||||
{
|
{
|
||||||
updateState();
|
updateState();
|
||||||
if (mp.pki_encrypted == false) {
|
// Note: pki_encrypted is not required here. The first response (M2) may arrive channel-encrypted in
|
||||||
return false;
|
// the bootstrap case; the follow-on hash1 packet (M3) is required to be PKI in its branch below.
|
||||||
}
|
|
||||||
if (mp.from != currentRemoteNode) { // because the inital connection request is handled in allocReply()
|
if (mp.from != currentRemoteNode) { // because the inital connection request is handled in allocReply()
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -74,9 +75,14 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (currentState == KEY_VERIFICATION_SENDER_HAS_INITIATED && r->nonce == currentNonce && r->hash2.size == 32 &&
|
if (currentState == KEY_VERIFICATION_SENDER_HAS_INITIATED && r->nonce == currentNonce && r->hash2.size == 32 &&
|
||||||
r->hash1.size == 0) {
|
r->hash1.size == 32) {
|
||||||
memcpy(hash2, r->hash2.bytes, 32);
|
memcpy(hash2, r->hash2.bytes, 32);
|
||||||
IF_SCREEN(screen->showNumberPicker("Enter Security Number", 60000, 6, [](int number_picked) -> void {
|
// The response carries the responder's public key in hash1. If we don't already hold it, stash it
|
||||||
|
// as a pending key so the Router can PKI-encrypt our follow-on packet (committed to NodeDB on accept).
|
||||||
|
auto *responderNode = nodeDB->getMeshNode(currentRemoteNode);
|
||||||
|
if (responderNode == nullptr || responderNode->public_key.size != 32)
|
||||||
|
crypto->setPendingPublicKey(currentRemoteNode, r->hash1.bytes);
|
||||||
|
IF_SCREEN(screen->showNumberPicker("Enter Security Number", 60000, 6, false, [](uint32_t number_picked) -> void {
|
||||||
keyVerificationModule->processSecurityNumber(number_picked);
|
keyVerificationModule->processSecurityNumber(number_picked);
|
||||||
});)
|
});)
|
||||||
|
|
||||||
@@ -95,7 +101,8 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
|
|||||||
currentState = KEY_VERIFICATION_SENDER_AWAITING_NUMBER;
|
currentState = KEY_VERIFICATION_SENDER_AWAITING_NUMBER;
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
} else if (currentState == KEY_VERIFICATION_RECEIVER_AWAITING_HASH1 && r->hash1.size == 32 && r->nonce == currentNonce) {
|
} else if (currentState == KEY_VERIFICATION_RECEIVER_AWAITING_HASH1 && mp.pki_encrypted && r->hash1.size == 32 &&
|
||||||
|
r->nonce == currentNonce) {
|
||||||
if (memcmp(hash1, r->hash1.bytes, 32) == 0) {
|
if (memcmp(hash1, r->hash1.bytes, 32) == 0) {
|
||||||
memset(message, 0, sizeof(message));
|
memset(message, 0, sizeof(message));
|
||||||
sprintf(message, "Verification: \n");
|
sprintf(message, "Verification: \n");
|
||||||
@@ -108,10 +115,9 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
|
|||||||
options.notificationType = graphics::notificationTypeEnum::selection_picker;
|
options.notificationType = graphics::notificationTypeEnum::selection_picker;
|
||||||
options.bannerCallback =
|
options.bannerCallback =
|
||||||
[=](int selected) {
|
[=](int selected) {
|
||||||
|
LOG_DEBUG("User selected %d for key verification", selected);
|
||||||
if (selected == 1) {
|
if (selected == 1) {
|
||||||
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
keyVerificationModule->commitVerifiedRemoteNode();
|
||||||
if (remoteNodePtr)
|
|
||||||
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
screen->showOverlayBanner(options);)
|
screen->showOverlayBanner(options);)
|
||||||
@@ -139,22 +145,29 @@ bool KeyVerificationModule::sendInitialRequest(NodeNum remoteNode)
|
|||||||
{
|
{
|
||||||
LOG_DEBUG("keyVerification start");
|
LOG_DEBUG("keyVerification start");
|
||||||
// generate nonce
|
// generate nonce
|
||||||
updateState();
|
updateState(false);
|
||||||
if (currentState != KEY_VERIFICATION_IDLE) {
|
if (currentState != KEY_VERIFICATION_IDLE) {
|
||||||
IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::ThrottleMessage;)
|
IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::ThrottleMessage;)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
updateState(true);
|
||||||
currentNonce = random();
|
currentNonce = random();
|
||||||
currentNonceTimestamp = getTime();
|
currentNonceTimestamp = getTime();
|
||||||
currentRemoteNode = remoteNode;
|
currentRemoteNode = remoteNode;
|
||||||
meshtastic_KeyVerification KeyVerification = meshtastic_KeyVerification_init_zero;
|
meshtastic_KeyVerification KeyVerification = meshtastic_KeyVerification_init_zero;
|
||||||
KeyVerification.nonce = currentNonce;
|
KeyVerification.nonce = currentNonce;
|
||||||
KeyVerification.hash2.size = 0;
|
KeyVerification.hash2.size = 0;
|
||||||
KeyVerification.hash1.size = 0;
|
// Carry our public key in the otherwise-unused hash1 field so a peer that does not yet hold our
|
||||||
|
// key can learn it from this first message (bootstrap / onboarding).
|
||||||
|
KeyVerification.hash1.size = 32;
|
||||||
|
memcpy(KeyVerification.hash1.bytes, owner.public_key.bytes, 32);
|
||||||
meshtastic_MeshPacket *p = allocDataProtobuf(KeyVerification);
|
meshtastic_MeshPacket *p = allocDataProtobuf(KeyVerification);
|
||||||
p->to = remoteNode;
|
p->to = remoteNode;
|
||||||
p->channel = 0;
|
p->channel = 0;
|
||||||
p->pki_encrypted = true;
|
// Only request PKI when we already hold the destination's key. Otherwise this first message goes out
|
||||||
|
// channel-encrypted (the Router falls back) so the peer can bootstrap from the key carried in hash1.
|
||||||
|
auto *remoteNodePtr = nodeDB->getMeshNode(remoteNode);
|
||||||
|
p->pki_encrypted = (remoteNodePtr != nullptr && remoteNodePtr->public_key.size == 32);
|
||||||
p->decoded.want_response = true;
|
p->decoded.want_response = true;
|
||||||
p->priority = meshtastic_MeshPacket_Priority_HIGH;
|
p->priority = meshtastic_MeshPacket_Priority_HIGH;
|
||||||
service->sendToMesh(p, RX_SRC_LOCAL, true);
|
service->sendToMesh(p, RX_SRC_LOCAL, true);
|
||||||
@@ -171,9 +184,6 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
|
|||||||
if (currentState != KEY_VERIFICATION_IDLE) { // TODO: cooldown period
|
if (currentState != KEY_VERIFICATION_IDLE) { // TODO: cooldown period
|
||||||
LOG_WARN("Key Verification requested, but already in a request");
|
LOG_WARN("Key Verification requested, but already in a request");
|
||||||
return nullptr;
|
return nullptr;
|
||||||
} else if (!currentRequest->pki_encrypted) {
|
|
||||||
LOG_WARN("Key Verification requested, but not in a PKI packet");
|
|
||||||
return nullptr;
|
|
||||||
}
|
}
|
||||||
currentState = KEY_VERIFICATION_RECEIVER_AWAITING_HASH1;
|
currentState = KEY_VERIFICATION_RECEIVER_AWAITING_HASH1;
|
||||||
|
|
||||||
@@ -188,15 +198,43 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
|
|||||||
response.nonce = scratch.nonce;
|
response.nonce = scratch.nonce;
|
||||||
currentRemoteNode = req.from;
|
currentRemoteNode = req.from;
|
||||||
currentNonceTimestamp = getTime();
|
currentNonceTimestamp = getTime();
|
||||||
currentSecurityNumber = random(1, 999999);
|
// The security number is the handshake's MitM-resistance entropy, so draw it from the hardware
|
||||||
|
// RNG (falling back to the CSPRNG used for key material), never the predictable random().
|
||||||
|
// Hold cryptLock like xeddsa_sign does: on nRF52 the fill toggles the CC310 that packet crypto
|
||||||
|
// on the BLE task also uses, and the CryptRNG state is shared with the signing path.
|
||||||
|
uint32_t securityEntropy = 0;
|
||||||
|
{
|
||||||
|
concurrency::LockGuard g(cryptLock);
|
||||||
|
if (!HardwareRNG::fill((uint8_t *)&securityEntropy, sizeof(securityEntropy)))
|
||||||
|
CryptRNG.rand((uint8_t *)&securityEntropy, sizeof(securityEntropy));
|
||||||
|
}
|
||||||
|
currentSecurityNumber = (securityEntropy % 999999) + 1;
|
||||||
|
|
||||||
// generate hash1
|
// Resolve the requester's public key: from the PKI envelope, else carried in hash1 (bootstrap).
|
||||||
|
// Stash unknown keys as pending (committed to NodeDB only once verification is accepted).
|
||||||
|
const uint8_t *senderKey = nullptr;
|
||||||
|
if (currentRequest->pki_encrypted && currentRequest->public_key.size == 32) {
|
||||||
|
senderKey = currentRequest->public_key.bytes; // this is bizarre, fixme
|
||||||
|
} else if (scratch.hash1.size == 32) {
|
||||||
|
senderKey = scratch.hash1.bytes;
|
||||||
|
}
|
||||||
|
if (senderKey == nullptr) {
|
||||||
|
LOG_WARN("Key Verification request without a usable public key");
|
||||||
|
resetToIdle();
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
auto *senderNode = nodeDB->getMeshNode(currentRemoteNode);
|
||||||
|
bool senderKeyInNodeDB = (senderNode != nullptr && senderNode->public_key.size == 32);
|
||||||
|
if (!senderKeyInNodeDB)
|
||||||
|
crypto->setPendingPublicKey(currentRemoteNode, senderKey);
|
||||||
|
|
||||||
|
// generate local hash1
|
||||||
hash.reset();
|
hash.reset();
|
||||||
hash.update(¤tSecurityNumber, sizeof(currentSecurityNumber));
|
hash.update(¤tSecurityNumber, sizeof(currentSecurityNumber));
|
||||||
hash.update(¤tNonce, sizeof(currentNonce));
|
hash.update(¤tNonce, sizeof(currentNonce));
|
||||||
hash.update(¤tRemoteNode, sizeof(currentRemoteNode));
|
hash.update(¤tRemoteNode, sizeof(currentRemoteNode));
|
||||||
hash.update(&ourNodeNum, sizeof(ourNodeNum));
|
hash.update(&ourNodeNum, sizeof(ourNodeNum));
|
||||||
hash.update(currentRequest->public_key.bytes, currentRequest->public_key.size);
|
hash.update(senderKey, 32);
|
||||||
hash.update(owner.public_key.bytes, owner.public_key.size);
|
hash.update(owner.public_key.bytes, owner.public_key.size);
|
||||||
hash.finalize(hash1, 32);
|
hash.finalize(hash1, 32);
|
||||||
|
|
||||||
@@ -205,15 +243,19 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
|
|||||||
hash.update(¤tNonce, sizeof(currentNonce));
|
hash.update(¤tNonce, sizeof(currentNonce));
|
||||||
hash.update(hash1, 32);
|
hash.update(hash1, 32);
|
||||||
hash.finalize(hash2, 32);
|
hash.finalize(hash2, 32);
|
||||||
response.hash1.size = 0;
|
// Carry our public key in hash1 of the response so the requester can bootstrap our key as well.
|
||||||
|
response.hash1.size = 32;
|
||||||
|
memcpy(response.hash1.bytes, owner.public_key.bytes, 32);
|
||||||
response.hash2.size = 32;
|
response.hash2.size = 32;
|
||||||
memcpy(response.hash2.bytes, hash2, 32);
|
memcpy(response.hash2.bytes, hash2, 32);
|
||||||
|
|
||||||
responsePacket = allocDataProtobuf(response);
|
responsePacket = allocDataProtobuf(response);
|
||||||
|
|
||||||
responsePacket->pki_encrypted = true;
|
// PKI-encrypt the response only if we already held the requester's key. In the bootstrap case it goes
|
||||||
|
// out channel-encrypted so the requester (who lacks our key) can decode it and read hash1.
|
||||||
|
responsePacket->pki_encrypted = senderKeyInNodeDB;
|
||||||
IF_SCREEN(snprintf(message, 25, "Security Number \n%03u %03u", currentSecurityNumber / 1000, currentSecurityNumber % 1000);
|
IF_SCREEN(snprintf(message, 25, "Security Number \n%03u %03u", currentSecurityNumber / 1000, currentSecurityNumber % 1000);
|
||||||
screen->showSimpleBanner(message, 30000); LOG_WARN("%s", message);)
|
screen->showSimpleBanner(message, 30000); LOG_DEBUG("%s", message);)
|
||||||
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
||||||
if (cn) {
|
if (cn) {
|
||||||
cn->level = meshtastic_LogRecord_Level_WARNING;
|
cn->level = meshtastic_LogRecord_Level_WARNING;
|
||||||
@@ -227,7 +269,7 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
|
|||||||
cn->payload_variant.key_verification_number_inform.security_number = currentSecurityNumber;
|
cn->payload_variant.key_verification_number_inform.security_number = currentSecurityNumber;
|
||||||
service->sendClientNotification(cn);
|
service->sendClientNotification(cn);
|
||||||
}
|
}
|
||||||
LOG_WARN("Security Number %04u, nonce %llu", currentSecurityNumber, currentNonce);
|
LOG_DEBUG("Security Number %04u, nonce %llu", currentSecurityNumber, currentNonce);
|
||||||
return responsePacket;
|
return responsePacket;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,14 +278,18 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
|
|||||||
SHA256 hash;
|
SHA256 hash;
|
||||||
NodeNum ourNodeNum = nodeDB->getNodeNum();
|
NodeNum ourNodeNum = nodeDB->getNodeNum();
|
||||||
uint8_t scratch_hash[32] = {0};
|
uint8_t scratch_hash[32] = {0};
|
||||||
LOG_WARN("received security number: %u", incomingNumber);
|
LOG_DEBUG("received security number: %u", incomingNumber);
|
||||||
meshtastic_NodeInfoLite *remoteNodePtr = nullptr;
|
meshtastic_NodeInfoLite *remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
||||||
remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
// Resolve the remote public key: NodeDB if known, otherwise the pending key learned during this
|
||||||
if (!remoteNodePtr || !nodeInfoLiteHasUser(remoteNodePtr) || remoteNodePtr->public_key.size != 32) {
|
// handshake (bootstrap case).
|
||||||
currentState = KEY_VERIFICATION_IDLE;
|
meshtastic_NodeInfoLite_public_key_t remotePublic = {0, {0}};
|
||||||
return; // should we throw an error here?
|
if (remoteNodePtr != nullptr && remoteNodePtr->public_key.size == 32) {
|
||||||
|
remotePublic = remoteNodePtr->public_key;
|
||||||
|
} else if (!crypto->getPendingPublicKey(currentRemoteNode, remotePublic)) {
|
||||||
|
LOG_WARN("No public key available for remote node, aborting key verification");
|
||||||
|
resetToIdle();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
LOG_WARN("hashing ");
|
|
||||||
// calculate hash1
|
// calculate hash1
|
||||||
hash.reset();
|
hash.reset();
|
||||||
hash.update(&incomingNumber, sizeof(incomingNumber));
|
hash.update(&incomingNumber, sizeof(incomingNumber));
|
||||||
@@ -252,7 +298,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
|
|||||||
hash.update(¤tRemoteNode, sizeof(currentRemoteNode));
|
hash.update(¤tRemoteNode, sizeof(currentRemoteNode));
|
||||||
hash.update(owner.public_key.bytes, owner.public_key.size);
|
hash.update(owner.public_key.bytes, owner.public_key.size);
|
||||||
|
|
||||||
hash.update(remoteNodePtr->public_key.bytes, remoteNodePtr->public_key.size);
|
hash.update(remotePublic.bytes, 32);
|
||||||
hash.finalize(hash1, 32);
|
hash.finalize(hash1, 32);
|
||||||
|
|
||||||
hash.reset();
|
hash.reset();
|
||||||
@@ -297,13 +343,13 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void KeyVerificationModule::updateState()
|
void KeyVerificationModule::updateState(bool resetTimer)
|
||||||
{
|
{
|
||||||
if (currentState != KEY_VERIFICATION_IDLE) {
|
if (currentState != KEY_VERIFICATION_IDLE) {
|
||||||
// check for the 60 second timeout
|
// check for the 60 second timeout
|
||||||
if (currentNonceTimestamp < getTime() - 60) {
|
if (currentNonceTimestamp < getTime() - 60) {
|
||||||
resetToIdle();
|
resetToIdle();
|
||||||
} else {
|
} else if (resetTimer) {
|
||||||
currentNonceTimestamp = getTime();
|
currentNonceTimestamp = getTime();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -318,6 +364,32 @@ void KeyVerificationModule::resetToIdle()
|
|||||||
currentSecurityNumber = 0;
|
currentSecurityNumber = 0;
|
||||||
currentRemoteNode = 0;
|
currentRemoteNode = 0;
|
||||||
currentState = KEY_VERIFICATION_IDLE;
|
currentState = KEY_VERIFICATION_IDLE;
|
||||||
|
// Discard any not-yet-verified key learned during this handshake; on reject/timeout it is never trusted.
|
||||||
|
crypto->clearPendingPublicKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
void KeyVerificationModule::commitVerifiedRemoteNode()
|
||||||
|
{
|
||||||
|
// The remote node already has a NodeDB entry by this point (packets were exchanged during the
|
||||||
|
// handshake), so getMeshNode is sufficient; bail defensively if it is somehow absent.
|
||||||
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(currentRemoteNode);
|
||||||
|
if (!node) {
|
||||||
|
LOG_WARN("Attempted to commit key, but unknown node");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// If we only held the peer's key as a pending (unverified) key during the handshake, commit it to
|
||||||
|
// NodeDB now that the user has confirmed the verification, so future PKI traffic can use it.
|
||||||
|
meshtastic_NodeInfoLite_public_key_t pending = {0, {0}};
|
||||||
|
if (node->public_key.size != 32 && crypto->getPendingPublicKey(currentRemoteNode, pending))
|
||||||
|
node->public_key = pending;
|
||||||
|
node->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||||
|
LOG_INFO("Node 0x%08x manually verified with security number %u", currentRemoteNode, currentSecurityNumber);
|
||||||
|
if (nodeInfoModule)
|
||||||
|
nodeInfoModule->sendOurNodeInfo(currentRemoteNode, false, node->channel, true);
|
||||||
|
crypto->clearPendingPublicKey();
|
||||||
|
currentState = KEY_VERIFICATION_IDLE;
|
||||||
|
// Persist the committed key and verified flag so manual verification survives a reboot.
|
||||||
|
nodeDB->saveToDisk(SEGMENT_NODEDATABASE);
|
||||||
}
|
}
|
||||||
|
|
||||||
void KeyVerificationModule::generateVerificationCode(char *readableCode)
|
void KeyVerificationModule::generateVerificationCode(char *readableCode)
|
||||||
|
|||||||
@@ -12,6 +12,39 @@ enum KeyVerificationState {
|
|||||||
KEY_VERIFICATION_RECEIVER_AWAITING_HASH1,
|
KEY_VERIFICATION_RECEIVER_AWAITING_HASH1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// KeyVerification Module overview
|
||||||
|
// This module allows for two useful functions. First, it implements a 2sv process that can manually verify a trustworthy
|
||||||
|
// connection with another node. It specifically verifies that the other node holds the correct private key for its public key, so
|
||||||
|
// it is resistant to MitM attacks. Second, it can be used to bootstrap trust in a new node by carrying the public key in the
|
||||||
|
// initial unencrypted message (in the hash1 field of the KeyVerification protobuf). This allows a user to manually verify a new
|
||||||
|
// node even if they don't have that node in the local nodeDB at all.
|
||||||
|
|
||||||
|
// The handshake process is as follows (NodeA = initiator, NodeB = responder):
|
||||||
|
// 1. NodeA sends a KeyVerification message containing a random nonce and its own public key (in the
|
||||||
|
// hash1 field) to NodeB. Implemented in sendInitialRequest(). It is PKI-encrypted if NodeA already
|
||||||
|
// holds NodeB's key, otherwise channel-encrypted (the bootstrap case).
|
||||||
|
//
|
||||||
|
// 2. NodeB replies (allocReply()) with its own public key (hash1 field) and hash2. NodeB generates a
|
||||||
|
// random 6-digit security number and stashes NodeA's public key (as a pending key if not already in
|
||||||
|
// the nodeDB). It computes hash1 = SHA256(securityNumber, nonce, NodeA_num, NodeB_num, PK_A, PK_B),
|
||||||
|
// then hash2 = SHA256(nonce, hash1). The reply is PKI-encrypted only if NodeB already held NodeA's
|
||||||
|
// key; in the bootstrap case it is channel-encrypted so NodeA can read NodeB's key from hash1.
|
||||||
|
//
|
||||||
|
// 3. NodeA receives the reply (handleReceivedProtobuf()), checks the nonce, stashes NodeB's public key,
|
||||||
|
// and prompts the user to enter the security number. The security number is never sent over the mesh
|
||||||
|
// and must be communicated over a secondary channel. processSecurityNumber() recomputes hash1 from
|
||||||
|
// the entered number and verifies SHA256(nonce, hash1) matches the received hash2. NodeA then sends
|
||||||
|
// its hash1 back to NodeB in a PKI-encrypted KeyVerification message (the follow-on PKI packet) and
|
||||||
|
// shows the KeyVerificationFinalPrompt menu, displaying 8 characters derived from hash1.
|
||||||
|
//
|
||||||
|
// 4. NodeB receives NodeA's hash1 (handleReceivedProtobuf(); required to be PKI-encrypted), checks it
|
||||||
|
// matches the hash1 NodeB generated, and shows the same 8-character code for final confirmation.
|
||||||
|
//
|
||||||
|
// The final on-screen code comparison is the actual manual verification: the user confirms the codes
|
||||||
|
// match on both devices, proving the two nodes agree on the same public keys (no MitM substitution).
|
||||||
|
// PKI-encrypting the follow-on packet additionally proves each node holds the private key for the
|
||||||
|
// agreed public key.
|
||||||
|
|
||||||
class KeyVerificationModule : public ProtobufModule<meshtastic_KeyVerification> //, private concurrency::OSThread //
|
class KeyVerificationModule : public ProtobufModule<meshtastic_KeyVerification> //, private concurrency::OSThread //
|
||||||
{
|
{
|
||||||
// CallbackObserver<KeyVerificationModule, const meshtastic::Status *> nodeStatusObserver =
|
// CallbackObserver<KeyVerificationModule, const meshtastic::Status *> nodeStatusObserver =
|
||||||
@@ -29,6 +62,7 @@ class KeyVerificationModule : public ProtobufModule<meshtastic_KeyVerification>
|
|||||||
bool sendInitialRequest(NodeNum remoteNode);
|
bool sendInitialRequest(NodeNum remoteNode);
|
||||||
void generateVerificationCode(char *); // fills char with the user readable verification code
|
void generateVerificationCode(char *); // fills char with the user readable verification code
|
||||||
uint32_t getCurrentRemoteNode() { return currentRemoteNode; }
|
uint32_t getCurrentRemoteNode() { return currentRemoteNode; }
|
||||||
|
void commitVerifiedRemoteNode(); // Commit a pending key to NodeDB and mark the node manually verified
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
/* Called to handle a particular incoming message
|
/* Called to handle a particular incoming message
|
||||||
@@ -58,8 +92,8 @@ class KeyVerificationModule : public ProtobufModule<meshtastic_KeyVerification>
|
|||||||
char message[40] = {0};
|
char message[40] = {0};
|
||||||
|
|
||||||
void processSecurityNumber(uint32_t);
|
void processSecurityNumber(uint32_t);
|
||||||
void updateState(); // check the timeouts and maybe reset the state to idle
|
void updateState(bool resetTimer = true); // check the timeouts and maybe reset the state to idle
|
||||||
void resetToIdle(); // Zero out module state
|
void resetToIdle(); // Zero out module state
|
||||||
};
|
};
|
||||||
|
|
||||||
extern KeyVerificationModule *keyVerificationModule;
|
extern KeyVerificationModule *keyVerificationModule;
|
||||||
@@ -2,11 +2,11 @@
|
|||||||
#include "Default.h"
|
#include "Default.h"
|
||||||
#include "DisplayFormatters.h"
|
#include "DisplayFormatters.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "RTC.h"
|
|
||||||
#include "RadioInterface.h"
|
#include "RadioInterface.h"
|
||||||
#include "Router.h"
|
#include "Router.h"
|
||||||
#include "TransmitHistory.h"
|
#include "TransmitHistory.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include <Throttle.h>
|
#include <Throttle.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|||||||
@@ -19,6 +19,9 @@
|
|||||||
#if !MESHTASTIC_EXCLUDE_CANNEDMESSAGES
|
#if !MESHTASTIC_EXCLUDE_CANNEDMESSAGES
|
||||||
#include "modules/CannedMessageModule.h"
|
#include "modules/CannedMessageModule.h"
|
||||||
#endif
|
#endif
|
||||||
|
#if HAS_SCREEN && BASEUI_HAS_GAMES
|
||||||
|
#include "modules/games/GamesModule.h"
|
||||||
|
#endif
|
||||||
#if !MESHTASTIC_EXCLUDE_DETECTIONSENSOR
|
#if !MESHTASTIC_EXCLUDE_DETECTIONSENSOR
|
||||||
#include "modules/DetectionSensorModule.h"
|
#include "modules/DetectionSensorModule.h"
|
||||||
#endif
|
#endif
|
||||||
@@ -206,6 +209,11 @@ void setupModules()
|
|||||||
cannedMessageModule = new CannedMessageModule();
|
cannedMessageModule = new CannedMessageModule();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
#if HAS_SCREEN && BASEUI_HAS_GAMES
|
||||||
|
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||||
|
gamesModule = new GamesModule();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
#if ARCH_PORTDUINO
|
#if ARCH_PORTDUINO
|
||||||
new HostMetricsModule();
|
new HostMetricsModule();
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
#include "Default.h"
|
#include "Default.h"
|
||||||
#include "MeshService.h"
|
#include "MeshService.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "RTC.h"
|
#include "gps/RTC.h"
|
||||||
#include <Throttle.h>
|
#include <Throttle.h>
|
||||||
|
|
||||||
NeighborInfoModule *neighborInfoModule;
|
NeighborInfoModule *neighborInfoModule;
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
#include "MeshService.h"
|
#include "MeshService.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "NodeStatus.h"
|
#include "NodeStatus.h"
|
||||||
#include "RTC.h"
|
|
||||||
#include "Router.h"
|
#include "Router.h"
|
||||||
#include "TransmitHistory.h"
|
#include "TransmitHistory.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include <Throttle.h>
|
#include <Throttle.h>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|||||||
@@ -5,13 +5,13 @@
|
|||||||
#include "MeshService.h"
|
#include "MeshService.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "PositionPrecision.h"
|
#include "PositionPrecision.h"
|
||||||
#include "RTC.h"
|
|
||||||
#include "Router.h"
|
#include "Router.h"
|
||||||
#include "TransmitHistory.h"
|
#include "TransmitHistory.h"
|
||||||
#include "TypeConversions.h"
|
#include "TypeConversions.h"
|
||||||
#include "airtime.h"
|
#include "airtime.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#include "gps/GeoCoord.h"
|
#include "gps/GeoCoord.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "meshUtils.h"
|
#include "meshUtils.h"
|
||||||
#include "meshtastic/atak.pb.h"
|
#include "meshtastic/atak.pb.h"
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
#include "MeshService.h"
|
#include "MeshService.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "PowerMon.h"
|
#include "PowerMon.h"
|
||||||
#include "RTC.h"
|
|
||||||
#include "Router.h"
|
#include "Router.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "sleep.h"
|
#include "sleep.h"
|
||||||
#include "target_specific.h"
|
#include "target_specific.h"
|
||||||
|
|||||||
@@ -13,12 +13,12 @@
|
|||||||
#include "MeshService.h"
|
#include "MeshService.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "PowerFSM.h"
|
#include "PowerFSM.h"
|
||||||
#include "RTC.h"
|
|
||||||
#include "Router.h"
|
#include "Router.h"
|
||||||
#include "SPILock.h"
|
#include "SPILock.h"
|
||||||
#include "airtime.h"
|
#include "airtime.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#include "gps/GeoCoord.h"
|
#include "gps/GeoCoord.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <Throttle.h>
|
#include <Throttle.h>
|
||||||
|
|
||||||
@@ -154,7 +154,7 @@ ProcessMessage RangeTestModuleRadio::handleReceived(const meshtastic_MeshPacket
|
|||||||
NodeInfoLite *n = nodeDB->getMeshNode(getFrom(&mp));
|
NodeInfoLite *n = nodeDB->getMeshNode(getFrom(&mp));
|
||||||
|
|
||||||
LOG_DEBUG("-----------------------------------------");
|
LOG_DEBUG("-----------------------------------------");
|
||||||
LOG_DEBUG("p.payload.bytes \"%s\"", p.payload.bytes);
|
LOG_DEBUG("p.payload.bytes \"%.*s\"", (int)p.payload.size, p.payload.bytes);
|
||||||
LOG_DEBUG("p.payload.size %d", p.payload.size);
|
LOG_DEBUG("p.payload.size %d", p.payload.size);
|
||||||
LOG_DEBUG("---- Received Packet:");
|
LOG_DEBUG("---- Received Packet:");
|
||||||
LOG_DEBUG("mp.from %d", mp.from);
|
LOG_DEBUG("mp.from %d", mp.from);
|
||||||
@@ -192,7 +192,7 @@ bool RangeTestModuleRadio::appendFile(const meshtastic_MeshPacket &mp)
|
|||||||
meshtastic_NodeInfoLite *n = nodeDB->getMeshNode(getFrom(&mp));
|
meshtastic_NodeInfoLite *n = nodeDB->getMeshNode(getFrom(&mp));
|
||||||
/*
|
/*
|
||||||
LOG_DEBUG("-----------------------------------------");
|
LOG_DEBUG("-----------------------------------------");
|
||||||
LOG_DEBUG("p.payload.bytes \"%s\"", p.payload.bytes);
|
LOG_DEBUG("p.payload.bytes \"%.*s\"", (int)p.payload.size, p.payload.bytes);
|
||||||
LOG_DEBUG("p.payload.size %d", p.payload.size);
|
LOG_DEBUG("p.payload.size %d", p.payload.size);
|
||||||
LOG_DEBUG("---- Received Packet:");
|
LOG_DEBUG("---- Received Packet:");
|
||||||
LOG_DEBUG("mp.from %d", mp.from);
|
LOG_DEBUG("mp.from %d", mp.from);
|
||||||
@@ -307,7 +307,7 @@ bool RangeTestModuleRadio::appendFile(const meshtastic_MeshPacket &mp)
|
|||||||
fileToAppend.printf("%d,", mp.hop_limit); // Packet Hop Limit
|
fileToAppend.printf("%d,", mp.hop_limit); // Packet Hop Limit
|
||||||
|
|
||||||
// TODO: If quotes are found in the payload, it has to be escaped.
|
// TODO: If quotes are found in the payload, it has to be escaped.
|
||||||
fileToAppend.printf("\"%s\"\n", p.payload.bytes);
|
fileToAppend.printf("\"%.*s\"\n", (int)p.payload.size, p.payload.bytes);
|
||||||
fileToAppend.printf("%i,", mp.rx_rssi); // RX RSSI
|
fileToAppend.printf("%i,", mp.rx_rssi); // RX RSSI
|
||||||
|
|
||||||
fileToAppend.flush();
|
fileToAppend.flush();
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
#include "RemoteHardwareModule.h"
|
#include "RemoteHardwareModule.h"
|
||||||
#include "MeshService.h"
|
#include "MeshService.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "RTC.h"
|
|
||||||
#include "Router.h"
|
#include "Router.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include <Throttle.h>
|
#include <Throttle.h>
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
#include "MeshService.h"
|
#include "MeshService.h"
|
||||||
#include "NMEAWPL.h"
|
#include "NMEAWPL.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "RTC.h"
|
|
||||||
#include "Router.h"
|
#include "Router.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <Throttle.h>
|
#include <Throttle.h>
|
||||||
|
|
||||||
@@ -396,7 +396,7 @@ ProcessMessage SerialModuleRadio::handleReceived(const meshtastic_MeshPacket &mp
|
|||||||
lastRxID = mp.id;
|
lastRxID = mp.id;
|
||||||
// LOG_DEBUG("* * Message came this device");
|
// LOG_DEBUG("* * Message came this device");
|
||||||
// serialPrint->println("* * Message came this device");
|
// serialPrint->println("* * Message came this device");
|
||||||
serialPrint->printf("%s", p.payload.bytes);
|
serialPrint->printf("%.*s", (int)p.payload.size, p.payload.bytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -408,7 +408,7 @@ ProcessMessage SerialModuleRadio::handleReceived(const meshtastic_MeshPacket &mp
|
|||||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp));
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp));
|
||||||
const char *sender = nodeInfoLiteHasUser(node) ? node->short_name : "???";
|
const char *sender = nodeInfoLiteHasUser(node) ? node->short_name : "???";
|
||||||
serialPrint->println();
|
serialPrint->println();
|
||||||
serialPrint->printf("%s: %s", sender, p.payload.bytes);
|
serialPrint->printf("%s: %.*s", sender, (int)p.payload.size, p.payload.bytes);
|
||||||
serialPrint->println();
|
serialPrint->println();
|
||||||
} else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA ||
|
} else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA ||
|
||||||
moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO) &&
|
moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO) &&
|
||||||
|
|||||||
@@ -15,11 +15,11 @@
|
|||||||
#include "StoreForwardModule.h"
|
#include "StoreForwardModule.h"
|
||||||
#include "MeshService.h"
|
#include "MeshService.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "RTC.h"
|
|
||||||
#include "Router.h"
|
#include "Router.h"
|
||||||
#include "Throttle.h"
|
#include "Throttle.h"
|
||||||
#include "airtime.h"
|
#include "airtime.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
#include "memGet.h"
|
#include "memGet.h"
|
||||||
#include "mesh-pb-constants.h"
|
#include "mesh-pb-constants.h"
|
||||||
#include "mesh/generated/meshtastic/storeforward.pb.h"
|
#include "mesh/generated/meshtastic/storeforward.pb.h"
|
||||||
|
|||||||
@@ -9,11 +9,11 @@
|
|||||||
#include "MeshService.h"
|
#include "MeshService.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "PowerFSM.h"
|
#include "PowerFSM.h"
|
||||||
#include "RTC.h"
|
|
||||||
#include "Router.h"
|
#include "Router.h"
|
||||||
#include "TransmitHistory.h"
|
#include "TransmitHistory.h"
|
||||||
#include "UnitConversions.h"
|
#include "UnitConversions.h"
|
||||||
#include "detect/ScanI2CTwoWire.h"
|
#include "detect/ScanI2CTwoWire.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
#include "graphics/ScreenFonts.h"
|
#include "graphics/ScreenFonts.h"
|
||||||
#include "graphics/SharedUIDisplay.h"
|
#include "graphics/SharedUIDisplay.h"
|
||||||
#include "graphics/images.h"
|
#include "graphics/images.h"
|
||||||
|
|||||||
@@ -4,11 +4,11 @@
|
|||||||
#include "MeshService.h"
|
#include "MeshService.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "PowerFSM.h"
|
#include "PowerFSM.h"
|
||||||
#include "RTC.h"
|
|
||||||
#include "RadioLibInterface.h"
|
#include "RadioLibInterface.h"
|
||||||
#include "Router.h"
|
#include "Router.h"
|
||||||
#include "TransmitHistory.h"
|
#include "TransmitHistory.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "memGet.h"
|
#include "memGet.h"
|
||||||
#include <OLEDDisplay.h>
|
#include <OLEDDisplay.h>
|
||||||
|
|||||||
@@ -9,11 +9,11 @@
|
|||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "Power.h"
|
#include "Power.h"
|
||||||
#include "PowerFSM.h"
|
#include "PowerFSM.h"
|
||||||
#include "RTC.h"
|
|
||||||
#include "Router.h"
|
#include "Router.h"
|
||||||
#include "TransmitHistory.h"
|
#include "TransmitHistory.h"
|
||||||
#include "UnitConversions.h"
|
#include "UnitConversions.h"
|
||||||
#include "buzz.h"
|
#include "buzz.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
#include "graphics/SharedUIDisplay.h"
|
#include "graphics/SharedUIDisplay.h"
|
||||||
#include "graphics/images.h"
|
#include "graphics/images.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
|
|||||||
@@ -9,10 +9,10 @@
|
|||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include "Power.h"
|
#include "Power.h"
|
||||||
#include "PowerFSM.h"
|
#include "PowerFSM.h"
|
||||||
#include "RTC.h"
|
|
||||||
#include "Router.h"
|
#include "Router.h"
|
||||||
#include "TransmitHistory.h"
|
#include "TransmitHistory.h"
|
||||||
#include "UnitConversions.h"
|
#include "UnitConversions.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "sleep.h"
|
#include "sleep.h"
|
||||||
#include "target_specific.h"
|
#include "target_specific.h"
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
#include "Power.h"
|
#include "Power.h"
|
||||||
#include "PowerFSM.h"
|
#include "PowerFSM.h"
|
||||||
#include "PowerTelemetry.h"
|
#include "PowerTelemetry.h"
|
||||||
#include "RTC.h"
|
|
||||||
#include "Router.h"
|
#include "Router.h"
|
||||||
#include "TransmitHistory.h"
|
#include "TransmitHistory.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
#include "graphics/SharedUIDisplay.h"
|
#include "graphics/SharedUIDisplay.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "sleep.h"
|
#include "sleep.h"
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
#include "../detect/ReClockI2C.h"
|
#include "../detect/ReClockI2C.h"
|
||||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||||
#include "RTC.h"
|
|
||||||
#include "TelemetrySensor.h"
|
#include "TelemetrySensor.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
|
|
||||||
#define PMSA003I_I2C_CLOCK_SPEED 100000
|
#define PMSA003I_I2C_CLOCK_SPEED 100000
|
||||||
#define PMSA003I_FRAME_LENGTH 32
|
#define PMSA003I_FRAME_LENGTH 32
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
#include "../detect/ReClockI2C.h"
|
#include "../detect/ReClockI2C.h"
|
||||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||||
#include "RTC.h"
|
|
||||||
#include "TelemetrySensor.h"
|
#include "TelemetrySensor.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
#include <SensirionI2cScd4x.h>
|
#include <SensirionI2cScd4x.h>
|
||||||
|
|
||||||
// Max speed 400kHz
|
// Max speed 400kHz
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
#include "../detect/ReClockI2C.h"
|
#include "../detect/ReClockI2C.h"
|
||||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||||
#include "RTC.h"
|
|
||||||
#include "TelemetrySensor.h"
|
#include "TelemetrySensor.h"
|
||||||
#include "Wire.h"
|
#include "Wire.h"
|
||||||
|
#include "gps/RTC.h"
|
||||||
|
|
||||||
// Warm up times for SEN5X from the datasheet
|
// Warm up times for SEN5X from the datasheet
|
||||||
#ifndef SEN5X_WARMUP_MS_1
|
#ifndef SEN5X_WARMUP_MS_1
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user