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.
|
||||
|
||||
### MCP tool surface (43 tools)
|
||||
### MCP tool surface (44 tools)
|
||||
|
||||
Grouped by purpose. Full argument shapes in the meshtastic-mcp repo's README; a few high-value signatures are called out here.
|
||||
|
||||
@@ -771,7 +771,7 @@ Grouped by purpose. Full argument shapes in the meshtastic-mcp repo's README; a
|
||||
- **Build & flash**: `build`, `clean`, `pio_flash`, `erase_and_flash` (ESP32 only), `update_flash` (ESP32 OTA), `touch_1200bps`
|
||||
- **Serial sessions** (long-running, 10k-line ring buffer): `serial_open`, `serial_read`, `serial_list`, `serial_close`
|
||||
- **Device reads**: `device_info`, `list_nodes`
|
||||
- **Device writes**: `set_owner`, `get_config`, `set_config`, `get_channel_url`, `set_channel_url`, `send_text`, `send_input_event` (inject a button/key press via the firmware's InputBroker), `set_debug_log_api`; destructive/power-state writes require `confirm=True`: `reboot`, `shutdown`, `factory_reset`
|
||||
- **Device writes**: `set_owner`, `get_config`, `set_config`, `get_channel_url`, `set_channel_url`, `send_text`, `send_input_event` (inject a button/key press via the firmware's InputBroker), `inject_frame` (inject an over-the-air-style frame into the RX pipeline - see below), `set_debug_log_api`; destructive/power-state writes require `confirm=True`: `reboot`, `shutdown`, `factory_reset`
|
||||
- **userPrefs admin** (build-time constants, not runtime config): `userprefs_get`, `userprefs_set`, `userprefs_reset`, `userprefs_manifest`, `userprefs_testing_profile`
|
||||
- **Vendor escape hatches**: `esptool_chip_info`, `esptool_erase_flash`, `esptool_raw`, `nrfutil_dfu`, `nrfutil_raw`, `picotool_info`, `picotool_load`, `picotool_raw`
|
||||
- **USB power control** (via `uhubctl`, per-port PPPS toggle): `uhubctl_list` (read-only), `uhubctl_power(action='on'|'off', confirm=True)`, `uhubctl_cycle(delay_s, confirm=True)`. Target by raw `(location, port)` or by `role` (`"nrf52"`, `"esp32s3"`); role lookup checks `MESHTASTIC_UHUBCTL_LOCATION_<ROLE>` + `_PORT_<ROLE>` env vars first, falls back to VID auto-detection.
|
||||
@@ -781,6 +781,15 @@ Grouped by purpose. Full argument shapes in the meshtastic-mcp repo's README; a
|
||||
|
||||
**TCP / native-host nodes.** Setting `MESHTASTIC_MCP_TCP_HOST=<host[:port]>` makes `list_devices` surface a `meshtasticd` daemon (e.g. the `native-macos` build) as a synthetic `tcp://host:port` entry, and `connect()` routes through `meshtastic.tcp_interface.TCPInterface` instead of `SerialInterface`. Every read/write/admin tool that flows through `connect()` works against the daemon transparently. USB-only tools (`pio_flash`, `erase_and_flash`, `update_flash`, `touch_1200bps`, `serial_open`, `esptool_*`, `nrfutil_*`, `picotool_*`) raise a clear `ConnectionError` when handed a `tcp://` port; `pio_flash` against a `native*` env raises a `FlashError` (no upload step - use `build` and run the binary directly). The pytest harness still assumes USB-attached devices per role; TCP-aware fixtures are deferred. See the meshtastic-mcp repo's README § "TCP / native-host nodes".
|
||||
|
||||
### Frame injection: testing the off-air receive path
|
||||
|
||||
The `toRadio` API can only inject **locally-originated** traffic - the firmware forces `p.from = 0` in `MeshService::handleToRadio`, which bypasses the `from != 0` receive path and everything gated on it (remote admin authorization, the admin session-passkey check, hop handling, promiscuous sniffing). To exercise those paths you either need a second transmitting radio, or **frame injection**: a build-flagged seam that delivers a client-supplied frame into the real RX pipeline as if it arrived off the LoRa chip.
|
||||
|
||||
- **Firmware:** build with `-D MESHTASTIC_ENABLE_FRAME_INJECTION=1` (`src/configuration.h`, off by default - it forges over-the-air traffic and must never ship enabled). `MeshService::injectAsReceived` extends the existing portduino `SimRadio` `SIMULATOR_APP` path to real hardware: it unwraps a `Compressed` envelope (`portnum == UNKNOWN_APP` → verbatim ciphertext the router decrypts; else → decoded payload for that portnum), then calls `router->enqueueReceivedMessage()` - the exact entry point `RadioLibInterface::handleReceiveInterrupt` uses. Injection is reached before the `p.from = 0` line, so a forged sender survives; `from == 0` is dropped to match real RX.
|
||||
- **Host:** drive it with the meshtastic-mcp `inject_frame` tool (or `cli/meshinject.py`). The crafter replicates channel crypto (default-PSK expansion, `xorHash` channel hash, AES-CTR with the `packetId|from|0` nonce), so an encrypted frame decrypts on-device as if received. Modes: `text`, `raw`, `admin` (+ `pki`/`public_key` for the PKC-admin path), `ciphertext`, `fuzz` (malformed-frame decode-path/crash testing).
|
||||
- **Example - remote-admin session-key repro:** set the target's `admin_key[0]` to a key you hold, then inject an `admin` set_owner with `pki=true`, that key, and a stale `session_hex`. The node logs `PKC admin payload with authorized sender key` → `Expected session key: 00…` → `Admin message without session_key!` - the exact ndoo scenario, on real silicon. Capture logs via `set_debug_log_api` on the same connection.
|
||||
- **nRF52 gotcha:** the USB CDC wedges under rapid `SerialInterface` open/close churn (unrelated to injection) - keep setup + inject + log-capture on one connection; recover a hung board via a 1200 bps-touch DFU reflash.
|
||||
|
||||
### Hardware test suite (`run-tests.sh`, from a meshtastic-mcp checkout)
|
||||
|
||||
The wrapper auto-detects connected devices (VID → role map: `0x239A` → `nrf52`, `0x303A`/`0x10C4` → `esp32s3`), maps each role to a PlatformIO env (`nrf52` → `rak4631`, `esp32s3` → `heltec-v3`, overridable via `MESHTASTIC_MCP_ENV_<ROLE>`), then invokes pytest. Zero pre-flight config needed from the operator.
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
pio upgrade
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
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`
|
||||
- **Device reads**: `device_info`, `list_nodes`
|
||||
- **Device writes** (require `confirm=True`): `set_owner`, `get_config`, `set_config`, `get_channel_url`, `set_channel_url`, `send_text`, `reboot`, `shutdown`, `factory_reset`, `set_debug_log_api`
|
||||
- **Frame injection**: `inject_frame` - deliver a crafted frame into a board's real RX pipeline as if received off LoRa (reaches `from != 0` / decrypt / remote-admin paths the `toRadio` API can't). Needs firmware built with `-D MESHTASTIC_ENABLE_FRAME_INJECTION=1` (`MeshService::injectAsReceived`); sim nodes always. See the copilot-instructions **Frame injection** section.
|
||||
- **userPrefs admin**: `userprefs_get`, `userprefs_set`, `userprefs_reset`, `userprefs_manifest`, `userprefs_testing_profile`
|
||||
- **Vendor escape hatches**: `esptool_*`, `nrfutil_*`, `picotool_*`
|
||||
|
||||
|
||||
+24
-1
@@ -153,6 +153,30 @@ Display:
|
||||
### You can also specify the spi device for the display to use
|
||||
# spidev: spidev0.0
|
||||
|
||||
### HUB75 RGB LED matrix panel (Raspberry Pi only, via hzeller/rpi-rgb-led-matrix).
|
||||
### Requires the librgbmatrix library to be installed (so `pkg-config rgbmatrix` resolves) and
|
||||
### the firmware built with it present. meshtasticd must run as root (or with /dev/gpiomem
|
||||
### access) and on-board audio disabled (dtparam=audio=off) so it doesn't fight the PWM timing.
|
||||
### The library owns the GPIO pins - they are chosen by HardwareMapping, not set individually.
|
||||
# Panel: HUB75
|
||||
# HUB75:
|
||||
# HardwareMapping: adafruit-hat-pwm # regular | adafruit-hat | adafruit-hat-pwm | regular-pi1 ...
|
||||
# Rows: 64 # pixel rows per panel (e.g. 32 or 64)
|
||||
# Cols: 64 # pixel columns per panel
|
||||
# ChainLength: 1 # panels daisy-chained (width = Cols * ChainLength)
|
||||
# Parallel: 1 # parallel chains (height = Rows * Parallel)
|
||||
# Brightness: 80 # percent, 1..100
|
||||
# PWMBits: 11 # lower = higher refresh, fewer colors
|
||||
# GPIOSlowdown: 4 # raise for faster Pis / long cables if the panel ghosts or flickers
|
||||
# RGBSequence: RGB # reorder if your panel's colors are swapped (e.g. RBG, BGR)
|
||||
# ScanMode: 0 # 0=progressive, 1=interlaced
|
||||
# RowAddressType: 0 # 0=default, 1=AB-addressed (some 64x64 panels)
|
||||
# Multiplexing: 0 # 0=direct, 1=stripe, 2=checker, ...
|
||||
# PanelType: "" # e.g. FM6126A for panels needing a special init
|
||||
# PixelMapper: "" # e.g. "U-mapper;Rotate:90"
|
||||
# LimitRefreshRateHz: 0 # 0 = no limit
|
||||
# DisableHardwarePulsing: false
|
||||
|
||||
Touchscreen:
|
||||
### Note, at least for now, the touchscreen must have a CS pin defined, even if you let Linux manage the CS switching.
|
||||
|
||||
@@ -167,7 +191,6 @@ Touchscreen:
|
||||
### You can also specify the spi device for the touchscreen to use
|
||||
# spidev: spidev0.0
|
||||
|
||||
|
||||
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"])])
|
||||
break
|
||||
|
||||
# DEBUG_MUTE already compiles every LOG_* call out entirely; nanopb's own error-message
|
||||
# strings are a separate mechanism it doesn't touch, so mirror the same intent into nanopb.
|
||||
debug_mute_defined = any(
|
||||
d == "DEBUG_MUTE" or (isinstance(d, tuple) and d[0] == "DEBUG_MUTE") for d in env.get("CPPDEFINES", [])
|
||||
)
|
||||
if debug_mute_defined:
|
||||
projenv.Append(CPPDEFINES=[("PB_NO_ERRMSG", 1)])
|
||||
for lb in env.GetLibBuilders():
|
||||
if lb.name == "Nanopb":
|
||||
lb.env.Append(CPPDEFINES=[("PB_NO_ERRMSG", 1)])
|
||||
break
|
||||
|
||||
# Get the display resolution from macros
|
||||
def get_display_resolution(build_flags):
|
||||
# Check "DISPLAY_SIZE" to determine the screen resolution
|
||||
|
||||
@@ -257,14 +257,14 @@ so they are stable as listed here:
|
||||
|
||||
`region_groups` (region → group_index):
|
||||
|
||||
| group | regions |
|
||||
| ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 0 | US, EU_433, CN, JP, ANZ, ANZ_433, RU, KR, TW, IN, NZ_865, TH, UA_433, UA_868, MY_433, MY_919, SG_923, PH_433, PH_868, PH_915, KZ_433, KZ_863, NP_865, BR_902, LORA_24 |
|
||||
| 1 | EU_868 |
|
||||
| 2 | EU_866 |
|
||||
| 3 | EU_N_868 |
|
||||
| 4 | ITU1_2M, ITU2_2M, ITU3_2M |
|
||||
| 5 | ITU2_125CM |
|
||||
| group | regions |
|
||||
| ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 0 | US, EU_433, CN, JP, ANZ, ANZ_433, RU, KR, TW, IN, NZ_865, TH, UA_433, MY_433, MY_919, SG_923, PH_433, PH_868, PH_915, KZ_433, KZ_863, NP_865, BR_902, LORA_24 |
|
||||
| 1 | EU_868 |
|
||||
| 2 | EU_866 |
|
||||
| 3 | EU_N_868 |
|
||||
| 4 | ITU1_2M, ITU2_2M, ITU3_2M |
|
||||
| 5 | ITU2_125CM |
|
||||
|
||||
> Note groups **3** and **5** carry the same preset list (NARROW\_\*) but are distinct groups
|
||||
> because they differ in `licensed_only`. Decoders must key on the group, not on the preset
|
||||
|
||||
+3
-3
@@ -69,7 +69,7 @@ monitor_speed = 115200
|
||||
monitor_filters = direct
|
||||
lib_deps =
|
||||
# renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master
|
||||
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/2e26010040e028baee72e2093402fa7b3c59e430.zip
|
||||
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/9d9ba7e43ed1a5e1865fc9686513eab47fb079ff.zip
|
||||
# renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master
|
||||
https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip
|
||||
# renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master
|
||||
@@ -127,7 +127,7 @@ lib_deps =
|
||||
[device-ui_base]
|
||||
lib_deps =
|
||||
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
|
||||
https://github.com/meshtastic/device-ui/archive/effbb925a7cbd3ab794dfcc5fa16228217d18408.zip
|
||||
https://github.com/meshtastic/device-ui/archive/33917d583eb3f4d6f93a24122a88101221afcc2d.zip
|
||||
|
||||
; Common libs for environmental measurements in telemetry module
|
||||
[environmental_base]
|
||||
@@ -191,7 +191,7 @@ lib_deps =
|
||||
# renovate: datasource=github-tags depName=Adafruit LTR390 Library packageName=adafruit/Adafruit_LTR390
|
||||
https://github.com/adafruit/Adafruit_LTR390/archive/refs/tags/1.1.2.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit PCT2075 packageName=adafruit/Adafruit_PCT2075
|
||||
https://github.com/adafruit/Adafruit_PCT2075/archive/refs/tags/1.0.6.zip
|
||||
https://github.com/adafruit/Adafruit_PCT2075/archive/1.3.1.zip
|
||||
# renovate: datasource=github-tags depName=DFRobot_BMM150 packageName=dfrobot/DFRobot_BMM150
|
||||
https://github.com/DFRobot/DFRobot_BMM150/archive/refs/tags/V1.0.0.zip
|
||||
# renovate: datasource=github-tags depName=SparkFun MMC5983MA Magnetometer packageName=sparkfun/SparkFun_MMC5983MA_Magnetometer_Arduino_Library
|
||||
|
||||
+1
-1
Submodule protobufs updated: 9d589c1321...f5b94bc367
@@ -57,7 +57,6 @@ using namespace Adafruit_LittleFS_Namespace;
|
||||
#endif
|
||||
|
||||
void fsInit();
|
||||
void fsListFiles();
|
||||
bool copyFile(const char *from, const char *to);
|
||||
bool renameFile(const char *pathFrom, const char *pathTo);
|
||||
bool fsFormat();
|
||||
|
||||
+1
-1
@@ -906,7 +906,7 @@ void Power::shutdown()
|
||||
#if HAS_SCREEN
|
||||
messageStore.saveToFlash();
|
||||
#endif
|
||||
#if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040)
|
||||
#if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)
|
||||
#ifdef PIN_LED1
|
||||
ledOff(PIN_LED1);
|
||||
#endif
|
||||
|
||||
@@ -51,9 +51,6 @@ class PowerStatus : public Status
|
||||
|
||||
bool getHasUSB() const { return hasUSB == OptTrue; }
|
||||
|
||||
/// Can we even know if this board has USB power or not
|
||||
bool knowsUSB() const { return hasUSB != OptUnknown; }
|
||||
|
||||
bool getIsCharging() const { return isCharging == OptTrue; }
|
||||
|
||||
int getBatteryVoltageMv() const { return batteryVoltageMv; }
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#include "RedirectablePrint.h"
|
||||
#include "NodeDB.h"
|
||||
#include "RTC.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "main.h"
|
||||
#include "memGet.h"
|
||||
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||
|
||||
@@ -30,6 +30,16 @@
|
||||
|
||||
SerialConsole *console;
|
||||
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
// Last-seen USB-CDC host link (DTR/mount) state, sampled each runOnce() so a
|
||||
// physical unplug/replug re-locks the per-connection admin auth (see runOnce()).
|
||||
// Kept at file scope rather than as a member both because there is exactly one
|
||||
// console singleton and because adding per-instance members to the PhoneAPI
|
||||
// hierarchy has historically perturbed nRF52 USB-CDC enumeration (see PhoneAPI.h).
|
||||
// Only compiled on lockdown (nRF52) builds.
|
||||
static bool s_serialLinkUp = false;
|
||||
#endif
|
||||
|
||||
/// Create the shared serial console once and register receive wakeups.
|
||||
void consoleInit()
|
||||
{
|
||||
@@ -88,6 +98,30 @@ SerialConsole::SerialConsole() : StreamAPI(&Port), RedirectablePrint(&Port), con
|
||||
/// Service one serial API iteration and select the next polling interval.
|
||||
int32_t SerialConsole::runOnce()
|
||||
{
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
// Lockdown (nRF52) builds only. The SerialConsole is a process-lifetime
|
||||
// singleton, so its inherited PhoneAPI object - and therefore its entry in
|
||||
// the per-connection admin-auth slot table (keyed by PhoneAPI*) - is reused
|
||||
// for every USB/serial client for the whole boot. Nothing re-locks that slot
|
||||
// when the operator unplugs and a different client plugs in before the
|
||||
// 15-minute inactivity timeout fires, so a fresh client would inherit the
|
||||
// prior operator's admin authorization. Re-lock when the physical USB-CDC link
|
||||
// drops - the serial analog of the BLE onDisconnect() -> close() session reset.
|
||||
//
|
||||
// On the nRF52 TinyUSB (Adafruit) core, (bool)Port == tud_cdc_n_connected():
|
||||
// it goes false on cable unplug or host port-close (DTR de-assert). close()
|
||||
// frees the auth slot and resets PhoneAPI state, so whoever connects next
|
||||
// re-locks via handleStartConfig()'s !isConnected() branch on their first
|
||||
// want_config - the same physical-link boundary BLE enforces in onConnect().
|
||||
// Console transports without a real DTR line (e.g. a UART USER_DEBUG_PORT) hold
|
||||
// this constant, so no edge fires and we fall back to the existing inactivity
|
||||
// timeout - no worse than the pre-fix behavior.
|
||||
const bool linkUp = static_cast<bool>(Port);
|
||||
if (s_serialLinkUp && !linkUp)
|
||||
close();
|
||||
s_serialLinkUp = linkUp;
|
||||
#endif
|
||||
|
||||
#ifdef HELTEC_MESH_SOLAR
|
||||
// After enabling the mesh solar serial port module configuration, command processing is handled by the serial port module.
|
||||
if (moduleConfig.serial.enabled && moduleConfig.serial.override_console_serial_port &&
|
||||
|
||||
@@ -88,6 +88,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define MESHTASTIC_PREHOP_DROP 1
|
||||
#endif
|
||||
|
||||
// Debug/test only: let a wired client (serial/TCP) inject frames into the RX pipeline as if they had
|
||||
// arrived over LoRa - a SIMULATOR_APP ToRadio packet is delivered through the real receive path on real
|
||||
// hardware (see MeshService::injectAsReceived). This forges over-the-air traffic, so it MUST stay 0 in
|
||||
// any shipping build; enable per-build with -D MESHTASTIC_ENABLE_FRAME_INJECTION=1.
|
||||
#ifndef MESHTASTIC_ENABLE_FRAME_INJECTION
|
||||
#define MESHTASTIC_ENABLE_FRAME_INJECTION 0
|
||||
#endif
|
||||
|
||||
/// Convert a preprocessor name into a quoted string
|
||||
#define xstr(s) ystr(s)
|
||||
#define ystr(s) #s
|
||||
@@ -416,6 +424,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#ifndef HAS_TFT
|
||||
#define HAS_TFT 0
|
||||
#endif
|
||||
// Opt-in: build the BaseUI games frame (Snake). Off by default; enable per build/variant with
|
||||
// -DBASEUI_HAS_GAMES=1 (requires HAS_SCREEN and a non-color BaseUI display).
|
||||
#ifndef BASEUI_HAS_GAMES
|
||||
#define BASEUI_HAS_GAMES 0
|
||||
#endif
|
||||
#ifndef HAS_WIRE
|
||||
#define HAS_WIRE 0
|
||||
#endif
|
||||
|
||||
+1
-1
@@ -8,10 +8,10 @@
|
||||
#include "GpioLogic.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerMon.h"
|
||||
#include "RTC.h"
|
||||
#include "Throttle.h"
|
||||
#include "buzz.h"
|
||||
#include "concurrency/Periodic.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "meshUtils.h"
|
||||
|
||||
#include "main.h" // pmu_found
|
||||
|
||||
+38
-5
@@ -36,19 +36,52 @@ GeoCoord::GeoCoord(double lat, double lon, int32_t alt) : _altitude(alt)
|
||||
GeoCoord::setCoords();
|
||||
}
|
||||
|
||||
// Initialize all the coordinate systems
|
||||
// Initialize DMS eagerly (cheap, most commonly used); UTM/MGRS/OSGR/OLC are computed lazily on
|
||||
// first access via their getters (see ensure*() below), since most callers never touch them.
|
||||
void GeoCoord::setCoords()
|
||||
{
|
||||
double lat = _latitude * 1e-7;
|
||||
double lon = _longitude * 1e-7;
|
||||
GeoCoord::latLongToDMS(lat, lon, _dms);
|
||||
GeoCoord::latLongToUTM(lat, lon, _utm);
|
||||
GeoCoord::latLongToMGRS(lat, lon, _mgrs);
|
||||
GeoCoord::latLongToOSGR(lat, lon, _osgr);
|
||||
GeoCoord::latLongToOLC(lat, lon, _olc);
|
||||
_utmValid = false;
|
||||
_mgrsValid = false;
|
||||
_osgrValid = false;
|
||||
_olcValid = false;
|
||||
_dirty = false;
|
||||
}
|
||||
|
||||
void GeoCoord::ensureUTM() const
|
||||
{
|
||||
if (!_utmValid) {
|
||||
GeoCoord::latLongToUTM(_latitude * 1e-7, _longitude * 1e-7, _utm);
|
||||
_utmValid = true;
|
||||
}
|
||||
}
|
||||
|
||||
void GeoCoord::ensureMGRS() const
|
||||
{
|
||||
if (!_mgrsValid) {
|
||||
GeoCoord::latLongToMGRS(_latitude * 1e-7, _longitude * 1e-7, _mgrs);
|
||||
_mgrsValid = true;
|
||||
}
|
||||
}
|
||||
|
||||
void GeoCoord::ensureOSGR() const
|
||||
{
|
||||
if (!_osgrValid) {
|
||||
GeoCoord::latLongToOSGR(_latitude * 1e-7, _longitude * 1e-7, _osgr);
|
||||
_osgrValid = true;
|
||||
}
|
||||
}
|
||||
|
||||
void GeoCoord::ensureOLC() const
|
||||
{
|
||||
if (!_olcValid) {
|
||||
GeoCoord::latLongToOLC(_latitude * 1e-7, _longitude * 1e-7, _olc);
|
||||
_olcValid = true;
|
||||
}
|
||||
}
|
||||
|
||||
void GeoCoord::updateCoords(int32_t lat, int32_t lon, int32_t alt)
|
||||
{
|
||||
// If marked dirty or new coordinates
|
||||
|
||||
+93
-23
@@ -65,14 +65,24 @@ class GeoCoord
|
||||
int32_t _altitude = 0;
|
||||
|
||||
DMS _dms = {};
|
||||
UTM _utm = {};
|
||||
MGRS _mgrs = {};
|
||||
OSGR _osgr = {};
|
||||
OLC _olc = {};
|
||||
// Computed lazily on first access via ensure*() below; mutable so const getters can populate
|
||||
// them on demand.
|
||||
mutable UTM _utm = {};
|
||||
mutable MGRS _mgrs = {};
|
||||
mutable OSGR _osgr = {};
|
||||
mutable OLC _olc = {};
|
||||
mutable bool _utmValid = false;
|
||||
mutable bool _mgrsValid = false;
|
||||
mutable bool _osgrValid = false;
|
||||
mutable bool _olcValid = false;
|
||||
|
||||
bool _dirty = true;
|
||||
|
||||
void setCoords();
|
||||
void ensureUTM() const;
|
||||
void ensureMGRS() const;
|
||||
void ensureOSGR() const;
|
||||
void ensureOLC() const;
|
||||
|
||||
public:
|
||||
GeoCoord();
|
||||
@@ -123,26 +133,86 @@ class GeoCoord
|
||||
uint32_t getDMSLonSec() const { return _dms.lonSec; }
|
||||
char getDMSLonCP() const { return _dms.lonCP; }
|
||||
|
||||
// UTM getters
|
||||
uint8_t getUTMZone() const { return _utm.zone; }
|
||||
char getUTMBand() const { return _utm.band; }
|
||||
uint32_t getUTMEasting() const { return _utm.easting; }
|
||||
uint32_t getUTMNorthing() const { return _utm.northing; }
|
||||
// UTM getters (computed on first access - see ensureUTM())
|
||||
uint8_t getUTMZone() const
|
||||
{
|
||||
ensureUTM();
|
||||
return _utm.zone;
|
||||
}
|
||||
char getUTMBand() const
|
||||
{
|
||||
ensureUTM();
|
||||
return _utm.band;
|
||||
}
|
||||
uint32_t getUTMEasting() const
|
||||
{
|
||||
ensureUTM();
|
||||
return _utm.easting;
|
||||
}
|
||||
uint32_t getUTMNorthing() const
|
||||
{
|
||||
ensureUTM();
|
||||
return _utm.northing;
|
||||
}
|
||||
|
||||
// MGRS getters
|
||||
uint8_t getMGRSZone() const { return _mgrs.zone; }
|
||||
char getMGRSBand() const { return _mgrs.band; }
|
||||
char getMGRSEast100k() const { return _mgrs.east100k; }
|
||||
char getMGRSNorth100k() const { return _mgrs.north100k; }
|
||||
uint32_t getMGRSEasting() const { return _mgrs.easting; }
|
||||
uint32_t getMGRSNorthing() const { return _mgrs.northing; }
|
||||
// MGRS getters (computed on first access - see ensureMGRS())
|
||||
uint8_t getMGRSZone() const
|
||||
{
|
||||
ensureMGRS();
|
||||
return _mgrs.zone;
|
||||
}
|
||||
char getMGRSBand() const
|
||||
{
|
||||
ensureMGRS();
|
||||
return _mgrs.band;
|
||||
}
|
||||
char getMGRSEast100k() const
|
||||
{
|
||||
ensureMGRS();
|
||||
return _mgrs.east100k;
|
||||
}
|
||||
char getMGRSNorth100k() const
|
||||
{
|
||||
ensureMGRS();
|
||||
return _mgrs.north100k;
|
||||
}
|
||||
uint32_t getMGRSEasting() const
|
||||
{
|
||||
ensureMGRS();
|
||||
return _mgrs.easting;
|
||||
}
|
||||
uint32_t getMGRSNorthing() const
|
||||
{
|
||||
ensureMGRS();
|
||||
return _mgrs.northing;
|
||||
}
|
||||
|
||||
// OSGR getters
|
||||
char getOSGRE100k() const { return _osgr.e100k; }
|
||||
char getOSGRN100k() const { return _osgr.n100k; }
|
||||
uint32_t getOSGREasting() const { return _osgr.easting; }
|
||||
uint32_t getOSGRNorthing() const { return _osgr.northing; }
|
||||
// OSGR getters (computed on first access - see ensureOSGR())
|
||||
char getOSGRE100k() const
|
||||
{
|
||||
ensureOSGR();
|
||||
return _osgr.e100k;
|
||||
}
|
||||
char getOSGRN100k() const
|
||||
{
|
||||
ensureOSGR();
|
||||
return _osgr.n100k;
|
||||
}
|
||||
uint32_t getOSGREasting() const
|
||||
{
|
||||
ensureOSGR();
|
||||
return _osgr.easting;
|
||||
}
|
||||
uint32_t getOSGRNorthing() const
|
||||
{
|
||||
ensureOSGR();
|
||||
return _osgr.northing;
|
||||
}
|
||||
|
||||
// OLC getter
|
||||
void getOLCCode(char *code) { strncpy(code, _olc.code, OLC_CODE_LEN + 1); } // +1 for null termination
|
||||
// OLC getter (computed on first access - see ensureOLC())
|
||||
void getOLCCode(char *code)
|
||||
{
|
||||
ensureOLC();
|
||||
strncpy(code, _olc.code, OLC_CODE_LEN + 1); // +1 for null termination
|
||||
}
|
||||
};
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
#if !MESHTASTIC_EXCLUDE_GPS
|
||||
#include "NMEAWPL.h"
|
||||
#include "GeoCoord.h"
|
||||
#include "RTC.h"
|
||||
#include "gps/RTC.h"
|
||||
#include <time.h>
|
||||
|
||||
/* -------------------------------------------
|
||||
|
||||
+33
-1
@@ -1,4 +1,4 @@
|
||||
#include "RTC.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "configuration.h"
|
||||
#include "detect/ScanI2C.h"
|
||||
#include "main.h"
|
||||
@@ -7,6 +7,10 @@
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
|
||||
#if HAS_LSE
|
||||
#include <STM32RTC.h>
|
||||
#endif
|
||||
|
||||
static RTCQuality currentQuality = RTCQualityNone;
|
||||
uint32_t lastSetFromPhoneNtpOrGps = 0;
|
||||
|
||||
@@ -211,6 +215,30 @@ RTCSetResult readFromRTC()
|
||||
return RTCSetResultSuccess;
|
||||
}
|
||||
}
|
||||
#elif HAS_LSE
|
||||
if (stm32wlRtcAvailable()) {
|
||||
uint32_t now = millis();
|
||||
tv.tv_sec = STM32RTC::getInstance().getEpoch();
|
||||
tv.tv_usec = 0;
|
||||
uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
|
||||
#ifdef BUILD_EPOCH
|
||||
if (tv.tv_sec < BUILD_EPOCH) {
|
||||
if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
|
||||
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
|
||||
lastTimeValidationWarning = millis();
|
||||
}
|
||||
return RTCSetResultInvalidTime;
|
||||
}
|
||||
#endif
|
||||
if (currentQuality == RTCQualityNone) {
|
||||
RTCQuality oldQuality = currentQuality;
|
||||
timeStartMsec = now;
|
||||
zeroOffsetSecs = tv.tv_sec;
|
||||
currentQuality = RTCQualityDevice;
|
||||
triggerNodeInfoCheckOnTimeSource(oldQuality, currentQuality);
|
||||
}
|
||||
return RTCSetResultSuccess;
|
||||
}
|
||||
#else
|
||||
return readFromSystemTimeFallback();
|
||||
#endif
|
||||
@@ -335,6 +363,10 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
|
||||
LOG_WARN("Failed to set time for RX8130CE");
|
||||
}
|
||||
}
|
||||
#elif HAS_LSE
|
||||
if (stm32wlRtcAvailable()) {
|
||||
STM32RTC::getInstance().setEpoch(tv->tv_sec);
|
||||
}
|
||||
#elif defined(ARCH_ESP32) || defined(ARCH_RP2040)
|
||||
settimeofday(tv, NULL);
|
||||
#endif
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
#include <ArtronShop_RX8130CE.h>
|
||||
#endif
|
||||
|
||||
#if HAS_LSE
|
||||
// True once the STM32WL LSE crystal has locked and the hardware RTC is running (see stm32wlSetup()).
|
||||
bool stm32wlRtcAvailable();
|
||||
#endif
|
||||
|
||||
enum RTCQuality {
|
||||
|
||||
/// We haven't had our RTC set yet
|
||||
|
||||
@@ -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
|
||||
#include "EInkParallelDisplay.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#if defined(USE_HUB75)
|
||||
#include "graphics/HUB75Display.h" // ESP32 HUB75 (I2S-DMA)
|
||||
#endif
|
||||
#if defined(HAS_HUB75_NATIVE)
|
||||
#include "HUB75Native.h" // native/Portduino HUB75 (rpi-rgb-led-matrix), from variants/native/portduino
|
||||
#endif
|
||||
|
||||
#include "DisplayFormatters.h"
|
||||
#include "TimeFormatters.h"
|
||||
@@ -67,6 +73,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#include "mesh/Default.h"
|
||||
#include "mesh/generated/meshtastic/deviceonly.pb.h"
|
||||
#include "modules/ExternalNotificationModule.h"
|
||||
#if BASEUI_HAS_GAMES
|
||||
#include "modules/games/GamesModule.h"
|
||||
#endif
|
||||
#include "modules/WaypointModule.h"
|
||||
#include "sleep.h"
|
||||
#include "target_specific.h"
|
||||
@@ -99,7 +108,11 @@ namespace graphics
|
||||
#define COMPASS_ACTIVE_FRAMERATE 20
|
||||
|
||||
// DEBUG
|
||||
#if BASEUI_HAS_GAMES
|
||||
#define NUM_EXTRA_FRAMES 4 // text message, debug frame, and the always-present games frame
|
||||
#else
|
||||
#define NUM_EXTRA_FRAMES 3 // text message and debug frame
|
||||
#endif
|
||||
// if defined a pixel will blink to show redraws
|
||||
// #define SHOW_REDRAWS
|
||||
#define ASCII_BELL '\x07'
|
||||
@@ -343,7 +356,7 @@ void Screen::showNodePicker(const char *message, uint32_t durationMs, std::funct
|
||||
}
|
||||
|
||||
// Called to trigger a banner with custom message and duration
|
||||
void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits,
|
||||
void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, bool useBase16,
|
||||
std::function<void(uint32_t)> bannerCallback)
|
||||
{
|
||||
#ifdef USE_EINK
|
||||
@@ -356,7 +369,10 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
|
||||
NotificationRenderer::alertBannerCallback = bannerCallback;
|
||||
NotificationRenderer::pauseBanner = false;
|
||||
NotificationRenderer::curSelected = 0;
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::number_picker;
|
||||
if (useBase16)
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::hex_picker;
|
||||
else
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::number_picker;
|
||||
NotificationRenderer::numDigits = digits;
|
||||
NotificationRenderer::currentNumber = 0;
|
||||
|
||||
@@ -366,6 +382,43 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
|
||||
updateUiFrame(ui);
|
||||
}
|
||||
|
||||
// Called to trigger an arcade-style initials picker (see showNumberPicker for the sibling flow).
|
||||
void Screen::showAlphanumericPicker(const char *message, const char *initialText, uint32_t durationMs, uint8_t length,
|
||||
std::function<void(const std::string &)> bannerCallback)
|
||||
{
|
||||
#ifdef USE_EINK
|
||||
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
|
||||
#endif
|
||||
if (length >= sizeof(NotificationRenderer::alphanumericValue))
|
||||
length = sizeof(NotificationRenderer::alphanumericValue) - 1;
|
||||
|
||||
strncpy(NotificationRenderer::alertBannerMessage, message, 255);
|
||||
NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination
|
||||
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
|
||||
NotificationRenderer::textInputCallback = bannerCallback;
|
||||
NotificationRenderer::pauseBanner = false;
|
||||
NotificationRenderer::curSelected = 0;
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::alphanumeric_picker;
|
||||
NotificationRenderer::numDigits = length;
|
||||
|
||||
// Seed each position from initialText (uppercased & filtered to A-Z/0-9), defaulting to 'A'.
|
||||
const size_t seedLen = initialText ? strnlen(initialText, length) : 0;
|
||||
for (uint8_t i = 0; i < length; i++) {
|
||||
char c = (i < seedLen) ? initialText[i] : 'A';
|
||||
if (c >= 'a' && c <= 'z')
|
||||
c = static_cast<char>(c - 'a' + 'A');
|
||||
if (!((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')))
|
||||
c = 'A';
|
||||
NotificationRenderer::alphanumericValue[i] = c;
|
||||
}
|
||||
NotificationRenderer::alphanumericValue[length] = '\0';
|
||||
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, 2);
|
||||
ui->setTargetFPS(60);
|
||||
updateUiFrame(ui);
|
||||
}
|
||||
|
||||
void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||
std::function<void(const std::string &)> textCallback)
|
||||
{
|
||||
@@ -409,6 +462,17 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int
|
||||
pi.drawFrame(display, state, x, y);
|
||||
}
|
||||
|
||||
#if BASEUI_HAS_GAMES
|
||||
// The games frame is a dedicated, always-present frame (unlike generic module frames it is placed
|
||||
// at a fixed position right after home), so it draws through its own trampoline rather than the
|
||||
// moduleFrames lockstep used by drawModuleFrame.
|
||||
static void drawGamesFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
if (gamesModule)
|
||||
gamesModule->drawFrame(display, state, x, y);
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Given a recent lat/lon return a guess of the heading the user is walking on.
|
||||
*
|
||||
@@ -496,6 +560,8 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
|
||||
#else
|
||||
dispdev = new ST7796Spi(&SPI1, ST7796_RESET, ST7796_RS, ST7796_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT);
|
||||
#endif
|
||||
#elif defined(USE_HUB75)
|
||||
dispdev = new HUB75Display(address.address, -1, -1, GEOMETRY_RAWMODE, HW_I2C::I2C_ONE);
|
||||
#elif defined(USE_SSD1306)
|
||||
dispdev = new SSD1306Wire(address.address, -1, -1, geometry,
|
||||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||||
@@ -514,7 +580,17 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
|
||||
LOG_INFO("SSD1306 init success");
|
||||
}
|
||||
#elif ARCH_PORTDUINO
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
|
||||
// HUB75 RGB matrix (hzeller/rpi-rgb-led-matrix) is a BaseUI framebuffer panel, selected at
|
||||
// runtime via config.yaml Display: Panel: HUB75.
|
||||
if (portduino_config.displayPanel == hub75) {
|
||||
#if defined(HAS_HUB75_NATIVE)
|
||||
LOG_DEBUG("Make HUB75Native!");
|
||||
dispdev = new HUB75Native(address.address, -1, -1, GEOMETRY_RAWMODE, HW_I2C::I2C_ONE);
|
||||
#else
|
||||
LOG_ERROR("HUB75 panel requested but rpi-rgb-led-matrix not compiled in!");
|
||||
#endif
|
||||
} else if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
if (portduino_config.displayPanel != no_screen) {
|
||||
LOG_DEBUG("Make TFTDisplay!");
|
||||
dispdev = new TFTDisplay(address.address, -1, -1, geometry,
|
||||
@@ -1301,6 +1377,15 @@ void Screen::setFrames(FrameFocus focus)
|
||||
indicatorIcons.push_back(icon_home);
|
||||
}
|
||||
|
||||
#if BASEUI_HAS_GAMES
|
||||
// Games frame: always present (even with no game running), positioned directly after home.
|
||||
if (gamesModule) {
|
||||
fsi.positions.games = numframes;
|
||||
normalFrames[numframes++] = drawGamesFrame;
|
||||
indicatorIcons.push_back(joystick_small);
|
||||
}
|
||||
#endif
|
||||
|
||||
fsi.positions.textMessage = numframes;
|
||||
normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame;
|
||||
indicatorIcons.push_back(icon_mail);
|
||||
@@ -2073,6 +2158,12 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
if (module && module->interceptingKeyboardInput())
|
||||
inputIntercepted = true;
|
||||
}
|
||||
#if BASEUI_HAS_GAMES
|
||||
// The games frame isn't a moduleFrame, so check it explicitly: while a game is running it
|
||||
// owns the D-pad (turns/pause) and we must not switch frames or open menus underneath it.
|
||||
if (gamesModule && gamesModule->interceptingKeyboardInput())
|
||||
inputIntercepted = true;
|
||||
#endif
|
||||
|
||||
// If no modules are using the input, move between frames
|
||||
if (!inputIntercepted) {
|
||||
@@ -2147,6 +2238,11 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
} else if (event->inputEvent == INPUT_BROKER_SELECT) {
|
||||
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
|
||||
menuHandler::homeBaseMenu();
|
||||
#if BASEUI_HAS_GAMES
|
||||
} else if (gamesModule && framesetInfo.positions.games != 255 &&
|
||||
this->ui->getUiState()->currentFrame == framesetInfo.positions.games) {
|
||||
gamesModule->launchGame(); // launch the game shown on the attract screen
|
||||
#endif
|
||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.system) {
|
||||
menuHandler::systemBaseMenu();
|
||||
#if HAS_GPS
|
||||
@@ -2214,6 +2310,11 @@ bool Screen::isOverlayBannerShowing()
|
||||
return NotificationRenderer::isOverlayBannerShowing();
|
||||
}
|
||||
|
||||
bool Screen::isGamesFrameShown()
|
||||
{
|
||||
return framesetInfo.positions.games != 255 && ui && ui->getUiState()->currentFrame == framesetInfo.positions.games;
|
||||
}
|
||||
|
||||
} // namespace graphics
|
||||
|
||||
#else
|
||||
|
||||
+15
-1
@@ -26,6 +26,9 @@ enum notificationTypeEnum {
|
||||
// LOCKED frame. Without this, a first-pair on a locked device cannot
|
||||
// complete because the PIN never renders.
|
||||
pairing_pin,
|
||||
// Arcade-style initials entry: like number_picker/hex_picker, but each position cycles
|
||||
// through A-Z and 0-9. The assembled string is returned via a text (std::string) callback.
|
||||
alphanumeric_picker,
|
||||
};
|
||||
|
||||
struct BannerOverlayOptions {
|
||||
@@ -284,6 +287,10 @@ class Screen : public concurrency::OSThread
|
||||
|
||||
bool isOverlayBannerShowing();
|
||||
|
||||
// True if the always-present games frame is the one currently on screen. Lets the games module
|
||||
// ignore D-pad input when the player has navigated to a different frame.
|
||||
bool isGamesFrameShown();
|
||||
|
||||
bool isScreenOn() { return screenOn; }
|
||||
|
||||
// Stores the last 4 of our hardware ID, to make finding the device for pairing easier
|
||||
@@ -344,7 +351,13 @@ class Screen : public concurrency::OSThread
|
||||
void showOverlayBanner(BannerOverlayOptions);
|
||||
|
||||
void showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback);
|
||||
void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function<void(uint32_t)> bannerCallback);
|
||||
void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, bool useBase16,
|
||||
std::function<void(uint32_t)> bannerCallback);
|
||||
// Arcade-style initials entry. `length` positions each cycle A-Z/0-9 (UP/DOWN), LEFT/RIGHT
|
||||
// moves the cursor, SELECT advances; the assembled string is delivered to `bannerCallback`.
|
||||
// `initialText` pre-seeds the positions (uppercased & filtered), defaulting to 'A'.
|
||||
void showAlphanumericPicker(const char *message, const char *initialText, uint32_t durationMs, uint8_t length,
|
||||
std::function<void(const std::string &)> bannerCallback);
|
||||
void showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||
std::function<void(const std::string &)> textCallback);
|
||||
|
||||
@@ -734,6 +747,7 @@ class Screen : public concurrency::OSThread
|
||||
uint8_t system = 255;
|
||||
uint8_t gps = 255;
|
||||
uint8_t home = 255;
|
||||
uint8_t games = 255;
|
||||
uint8_t textMessage = 255;
|
||||
uint8_t nodelist_nodes = 255;
|
||||
uint8_t nodelist_location = 255;
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "Power.h"
|
||||
#include "RTC.h"
|
||||
#include "draw/NodeListRenderer.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/TFTColorRegions.h"
|
||||
@@ -147,8 +147,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
// Transparent clock headers should inherit whatever body off-color is
|
||||
// already active under the header (important for light/inverted themes).
|
||||
const uint16_t transparentBgColor = resolveTFTOffColorAt(0, headerHeight + 1, getThemeBodyBg());
|
||||
setAndRegisterTFTColorRole(TFTColorRole::HeaderBackground, transparentBgColor, transparentBgColor, 0, 0, screenW,
|
||||
headerHeight);
|
||||
// Intentionally skip the HeaderBackground region, as small screens draw the clock in the unused space in this region,
|
||||
// and the transparent call was erasing segments from the clock
|
||||
setTFTColorRole(TFTColorRole::HeaderTitle, headerTitleColorForRole, transparentBgColor);
|
||||
setTFTColorRole(TFTColorRole::HeaderStatus, headerStatusColor, transparentBgColor);
|
||||
} else if (useInvertedHeaderStyle) {
|
||||
|
||||
@@ -16,8 +16,9 @@ struct TFTColorRegion {
|
||||
// Required by ST7789 driver: it scans until the first disabled entry.
|
||||
bool enabled = false;
|
||||
};
|
||||
|
||||
#ifndef MAX_TFT_COLOR_REGIONS
|
||||
static constexpr size_t MAX_TFT_COLOR_REGIONS = 48;
|
||||
#endif
|
||||
extern TFTColorRegion colorRegions[MAX_TFT_COLOR_REGIONS];
|
||||
|
||||
enum class TFTColorRole : uint8_t {
|
||||
@@ -39,7 +40,7 @@ enum class TFTColorRole : uint8_t {
|
||||
Count
|
||||
};
|
||||
|
||||
#if HAS_TFT || defined(HAS_SPI_TFT)
|
||||
#if HAS_TFT || defined(HAS_SPI_TFT) || defined(HAS_HUB75_NATIVE)
|
||||
#define GRAPHICS_TFT_COLORING_ENABLED 1
|
||||
#else
|
||||
#define GRAPHICS_TFT_COLORING_ENABLED 0
|
||||
|
||||
@@ -121,7 +121,6 @@ static void rak14014_tpIntHandle(void)
|
||||
|
||||
#elif defined(HACKADAY_COMMUNICATOR)
|
||||
#include <Arduino_GFX_Library.h>
|
||||
Arduino_DataBus *bus = nullptr;
|
||||
Arduino_GFX *tft = nullptr;
|
||||
|
||||
#elif defined(ST72xx_DE)
|
||||
@@ -1601,17 +1600,21 @@ bool TFTDisplay::connect()
|
||||
{
|
||||
concurrency::LockGuard g(spiLock);
|
||||
LOG_INFO("Do TFT init");
|
||||
// connect() re-runs on every display wake on variants whose handleSetOn() re-inits
|
||||
// the UI (see the gates in Screen::handleSetOn); construct the driver exactly once.
|
||||
if (!tft) {
|
||||
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) || defined(HELTEC_MESH_NODE_T1)
|
||||
tft = new TFT_eSPI;
|
||||
tft = new TFT_eSPI;
|
||||
#elif defined(HACKADAY_COMMUNICATOR)
|
||||
bus = new Arduino_ESP32SPI(TFT_DC, TFT_CS, 38 /* SCK */, 21 /* MOSI */, GFX_NOT_DEFINED /* MISO */, HSPI /* spi_num */);
|
||||
tft = new Arduino_NV3007(bus, 40, 0 /* rotation */, false /* IPS */, 142 /* width */, 428 /* height */, 12 /* col offset 1 */,
|
||||
0 /* row offset 1 */, 14 /* col offset 2 */, 0 /* row offset 2 */, nv3007_279_init_operations,
|
||||
sizeof(nv3007_279_init_operations));
|
||||
|
||||
Arduino_DataBus *bus =
|
||||
new Arduino_ESP32SPI(TFT_DC, TFT_CS, 38 /* SCK */, 21 /* MOSI */, GFX_NOT_DEFINED /* MISO */, HSPI /* spi_num */);
|
||||
tft = new Arduino_NV3007(bus, 40, 0 /* rotation */, false /* IPS */, 142 /* width */, 428 /* height */,
|
||||
12 /* col offset 1 */, 0 /* row offset 1 */, 14 /* col offset 2 */, 0 /* row offset 2 */,
|
||||
nv3007_279_init_operations, sizeof(nv3007_279_init_operations));
|
||||
#else
|
||||
tft = new LGFX;
|
||||
tft = new LGFX;
|
||||
#endif
|
||||
}
|
||||
|
||||
backlightEnable->set(true);
|
||||
LOG_INFO("Power to TFT Backlight");
|
||||
|
||||
@@ -743,17 +743,18 @@ void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int1
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
int line = 1;
|
||||
int scale = 1;
|
||||
int iconX = SCREEN_WIDTH - chirpy_width - (chirpy_width / 3);
|
||||
int iconY = (SCREEN_HEIGHT - chirpy_height) / 2;
|
||||
int textX_offset = 10;
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
textX_offset = textX_offset * 4;
|
||||
const int scale = 2;
|
||||
scale = 2;
|
||||
iconX = SCREEN_WIDTH - (chirpy_width * 2) - ((chirpy_width * 2) / 3);
|
||||
iconY = (SCREEN_HEIGHT - (chirpy_height * 2)) / 2;
|
||||
const int bytesPerRow = (chirpy_width + 7) / 8;
|
||||
|
||||
for (int yy = 0; yy < chirpy_height; ++yy) {
|
||||
iconX = SCREEN_WIDTH - (chirpy_width * 2) - ((chirpy_width * 2) / 3);
|
||||
iconY = (SCREEN_HEIGHT - (chirpy_height * 2)) / 2;
|
||||
const uint8_t *rowPtr = chirpy + yy * bytesPerRow;
|
||||
for (int xx = 0; xx < chirpy_width; ++xx) {
|
||||
const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3));
|
||||
@@ -767,6 +768,19 @@ void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int1
|
||||
display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy);
|
||||
}
|
||||
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
// Colour Chirpy on colour displays. The glyph is a filled head silhouette whose eyes are holes
|
||||
// (off pixels), so two stacked regions render the proper mascot without a second bitmap:
|
||||
// A) whole glyph -> green body / frame / legs
|
||||
// B) the eye band -> black face, with the eye holes turning white via the region's off-colour
|
||||
// Start the face band one column in from the head's left edge (col 6) so that edge stays green,
|
||||
// matching the green column already left on the right edge (col 31).
|
||||
graphics::registerTFTColorRegionDirect(iconX, iconY, chirpy_width * scale, chirpy_height * scale,
|
||||
graphics::TFTPalette::MeshtasticGreen, graphics::getThemeBodyBg());
|
||||
graphics::registerTFTColorRegionDirect(iconX + 7 * scale, iconY + 12 * scale, 24 * scale, 16 * scale,
|
||||
graphics::TFTPalette::Black, graphics::TFTPalette::White);
|
||||
#endif
|
||||
|
||||
int textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("Hello") / 2);
|
||||
display->drawString(textX, getTextPositions(display)[line++], "Hello");
|
||||
textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("World!") / 2);
|
||||
|
||||
@@ -242,7 +242,6 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||
{"TH", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_TH},
|
||||
{"LORA_24", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_LORA_24},
|
||||
{"UA_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_UA_433},
|
||||
{"UA_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_UA_868},
|
||||
{"MY_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_MY_433},
|
||||
{"MY_919", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_MY_919},
|
||||
{"SG_923", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_SG_923},
|
||||
@@ -2323,8 +2322,10 @@ void menuHandler::testMenu()
|
||||
|
||||
void menuHandler::numberTest()
|
||||
{
|
||||
screen->showNumberPicker("Pick a number\n ", 30000, 4,
|
||||
[](int number_picked) -> void { LOG_WARN("Nodenum: %u", number_picked); });
|
||||
screen->showNumberPicker("Verify Nodenum:\n ", 30000, 8, true, [](uint32_t number_picked) -> void {
|
||||
LOG_DEBUG("Nodenum: 0x%08x", number_picked);
|
||||
keyVerificationModule->sendInitialRequest(number_picked);
|
||||
});
|
||||
}
|
||||
|
||||
void menuHandler::wifiBaseMenu()
|
||||
@@ -2508,8 +2509,7 @@ void menuHandler::keyVerificationFinalPrompt()
|
||||
options.notificationType = graphics::notificationTypeEnum::selection_picker;
|
||||
options.bannerCallback = [=](int selected) {
|
||||
if (selected == 1) {
|
||||
auto remoteNodePtr = nodeDB->getMeshNode(keyVerificationModule->getCurrentRemoteNode());
|
||||
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||
keyVerificationModule->commitVerifiedRemoteNode();
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(options);
|
||||
|
||||
@@ -54,6 +54,7 @@ bool NotificationRenderer::pauseBanner = false;
|
||||
notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none;
|
||||
uint32_t NotificationRenderer::numDigits = 0;
|
||||
uint32_t NotificationRenderer::currentNumber = 0;
|
||||
char NotificationRenderer::alphanumericValue[16] = {0};
|
||||
VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr;
|
||||
std::function<void(const std::string &)> NotificationRenderer::textInputCallback = nullptr;
|
||||
|
||||
@@ -277,6 +278,9 @@ void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayU
|
||||
case notificationTypeEnum::hex_picker:
|
||||
drawHexPicker(display, state);
|
||||
break;
|
||||
case notificationTypeEnum::alphanumeric_picker:
|
||||
drawAlphanumericPicker(display, state);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,6 +466,96 @@ void NotificationRenderer::drawHexPicker(OLEDDisplay *display, OLEDDisplayUiStat
|
||||
drawNotificationBox(display, state, linePointers, totalLines, 0);
|
||||
}
|
||||
|
||||
// Arcade-style initials entry. Mirrors drawHexPicker's cursor/confirm flow, but each position
|
||||
// holds a character from ALPHANUMERIC_CHARS (cycled with UP/DOWN) instead of a packed digit, and
|
||||
// the assembled string is returned through textInputCallback.
|
||||
void NotificationRenderer::drawAlphanumericPicker(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||
{
|
||||
static const char ALPHANUMERIC_CHARS[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
constexpr int ALPHANUMERIC_COUNT = sizeof(ALPHANUMERIC_CHARS) - 1; // exclude the NUL
|
||||
|
||||
const char *lineStarts[MAX_LINES + 1] = {0};
|
||||
uint16_t lineCount = 0;
|
||||
|
||||
// Parse lines (identical to the number/hex pickers)
|
||||
char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage));
|
||||
lineStarts[lineCount] = alertBannerMessage;
|
||||
while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) {
|
||||
lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n');
|
||||
if (lineStarts[lineCount + 1][0] == '\n')
|
||||
lineStarts[lineCount + 1] += 1;
|
||||
lineCount++;
|
||||
}
|
||||
|
||||
auto alphaIndex = [&](char c) -> int {
|
||||
for (int i = 0; i < ALPHANUMERIC_COUNT; i++)
|
||||
if (ALPHANUMERIC_CHARS[i] == c)
|
||||
return i;
|
||||
return 0;
|
||||
};
|
||||
|
||||
// Handle input
|
||||
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS ||
|
||||
inEvent.inputEvent == INPUT_BROKER_UP_LONG) {
|
||||
int idx = (alphaIndex(alphanumericValue[curSelected]) + 1) % ALPHANUMERIC_COUNT;
|
||||
alphanumericValue[curSelected] = ALPHANUMERIC_CHARS[idx];
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS ||
|
||||
inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
|
||||
int idx = (alphaIndex(alphanumericValue[curSelected]) + ALPHANUMERIC_COUNT - 1) % ALPHANUMERIC_COUNT;
|
||||
alphanumericValue[curSelected] = ALPHANUMERIC_CHARS[idx];
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_ANYKEY) {
|
||||
char k = inEvent.kbchar;
|
||||
if (k >= 'a' && k <= 'z')
|
||||
k = static_cast<char>(k - 'a' + 'A');
|
||||
if ((k >= 'A' && k <= 'Z') || (k >= '0' && k <= '9')) { // direct keyboard entry
|
||||
alphanumericValue[curSelected] = k;
|
||||
curSelected++;
|
||||
}
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT || inEvent.inputEvent == INPUT_BROKER_RIGHT) {
|
||||
curSelected++;
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_LEFT) {
|
||||
curSelected--;
|
||||
} else if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) &&
|
||||
alertBannerUntil != 0) {
|
||||
resetBanner();
|
||||
return;
|
||||
}
|
||||
|
||||
if (curSelected < 0)
|
||||
curSelected = 0;
|
||||
if (curSelected == static_cast<int8_t>(numDigits)) {
|
||||
auto callback = textInputCallback; // capture before clearing to avoid re-entrancy surprises
|
||||
std::string result(alphanumericValue, numDigits);
|
||||
textInputCallback = nullptr;
|
||||
resetBanner();
|
||||
if (callback)
|
||||
callback(result);
|
||||
return;
|
||||
}
|
||||
|
||||
inEvent.inputEvent = INPUT_BROKER_NONE;
|
||||
if (alertBannerMessage[0] == '\0')
|
||||
return;
|
||||
|
||||
uint16_t totalLines = lineCount + 2;
|
||||
const char *linePointers[totalLines + 1] = {0}; // this is sort of a dynamic allocation
|
||||
|
||||
for (uint16_t i = 0; i < lineCount; i++) {
|
||||
linePointers[i] = lineStarts[i];
|
||||
}
|
||||
std::string chars = " ";
|
||||
std::string arrowPointer = " ";
|
||||
for (uint16_t i = 0; i < numDigits; i++) {
|
||||
chars += std::string(1, alphanumericValue[i]) + " ";
|
||||
arrowPointer += (curSelected == static_cast<int8_t>(i)) ? "^ " : "_ ";
|
||||
}
|
||||
|
||||
linePointers[lineCount++] = chars.c_str();
|
||||
linePointers[lineCount++] = arrowPointer.c_str();
|
||||
|
||||
drawNotificationBox(display, state, linePointers, totalLines, 0);
|
||||
}
|
||||
|
||||
void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||
{
|
||||
static uint32_t selectedNodenum = 0;
|
||||
|
||||
@@ -26,6 +26,7 @@ class NotificationRenderer
|
||||
static std::function<void(int)> alertBannerCallback;
|
||||
static uint32_t numDigits;
|
||||
static uint32_t currentNumber;
|
||||
static char alphanumericValue[16]; // working buffer for the alphanumeric_picker
|
||||
static VirtualKeyboard *virtualKeyboard;
|
||||
static std::function<void(const std::string &)> textInputCallback;
|
||||
|
||||
@@ -43,6 +44,7 @@ class NotificationRenderer
|
||||
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawHexPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawAlphanumericPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1],
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||
#include "modules/StatusMessageModule.h"
|
||||
#endif
|
||||
#if BASEUI_HAS_GAMES
|
||||
#include "modules/games/GamesModule.h"
|
||||
#endif
|
||||
#include "UIRenderer.h"
|
||||
#include "airtime.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
@@ -21,8 +24,8 @@
|
||||
#include "main.h"
|
||||
#include "target_specific.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#include <RTC.h>
|
||||
#include <cstring>
|
||||
#include <gps/RTC.h>
|
||||
|
||||
// External variables
|
||||
extern graphics::Screen *screen;
|
||||
@@ -1817,6 +1820,13 @@ constexpr uint32_t ICON_DISPLAY_DURATION_MS = 2000;
|
||||
// cppcheck-suppress constParameterPointer; signature must match OverlayCallback typedef from OLEDDisplayUi library
|
||||
void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||
{
|
||||
#if BASEUI_HAS_GAMES
|
||||
// Hide the navigation bar while a game owns the screen (the attract screen doesn't intercept,
|
||||
// so the nav bar stays visible there).
|
||||
if (gamesModule && gamesModule->interceptingKeyboardInput())
|
||||
return;
|
||||
#endif
|
||||
|
||||
uint8_t frameToHighlight = state->currentFrame;
|
||||
if (state->frameState == IN_TRANSITION && state->transitionFrameTarget < screen->indicatorIcons.size()) {
|
||||
frameToHighlight = state->transitionFrameTarget;
|
||||
|
||||
@@ -319,6 +319,46 @@ const uint8_t chirpy_small[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f};
|
||||
#define connection_icon_height 5
|
||||
const uint8_t connection_icon[] = {0x36, 0x41, 0x5D, 0x41, 0x36};
|
||||
|
||||
// Joystick icon (16x16): a round knob on a shaft over an elliptical base. Drawn on the Snake
|
||||
// attract screen as a "use the D-pad" controls hint.
|
||||
#define joystick_width 16
|
||||
#define joystick_height 16
|
||||
static const uint8_t joystick[] PROGMEM = {0xC0, 0x03, 0xE0, 0x07, 0xF0, 0x0F, 0xF0, 0x0F, 0xE0, 0x07, 0xC0,
|
||||
0x03, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0xF8, 0x1F,
|
||||
0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0xF8, 0x1F};
|
||||
|
||||
// 8x8 joystick, for the navigation bar (which draws 8x8 glyphs, scaled up on hi-res displays).
|
||||
#define joystick_small_width 8
|
||||
#define joystick_small_height 8
|
||||
static const uint8_t joystick_small[] PROGMEM = {0x18, 0x3C, 0x3C, 0x18, 0x7E, 0x99, 0xC3, 0x7E};
|
||||
|
||||
#define snake_width 16
|
||||
#define snake_height 16
|
||||
static const uint8_t snake[] PROGMEM = {0x80, 0x1F, 0x40, 0x20, 0x20, 0x48, 0x20, 0x40, 0xA0, 0x44, 0x20,
|
||||
0x39, 0x40, 0xDE, 0x86, 0x44, 0x0A, 0x19, 0x32, 0x22, 0xC4, 0x47,
|
||||
0x1C, 0x4D, 0x6A, 0x4A, 0xFA, 0x47, 0x04, 0x20, 0xF8, 0x1F};
|
||||
|
||||
#define tetris_width 16
|
||||
#define tetris_height 16
|
||||
static const uint8_t tetris[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x01, 0x80, 0xFD, 0xBF, 0xFD,
|
||||
0xBF, 0x81, 0x81, 0xBF, 0xFD, 0xA0, 0x05, 0xA0, 0x05, 0xA0, 0x05,
|
||||
0x20, 0x04, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
#define breakout_width 16
|
||||
#define breakout_height 16
|
||||
// Three staggered brick courses, a ball, and a paddle. XBM: 2 bytes/row, LSB = leftmost pixel.
|
||||
static const uint8_t breakout[] PROGMEM = {0x00, 0x00, 0x77, 0x77, 0x77, 0x77, 0x00, 0x00, 0xDD, 0xDD, 0xDD,
|
||||
0xDD, 0x00, 0x00, 0x77, 0x77, 0x77, 0x77, 0x00, 0x00, 0x80, 0x01,
|
||||
0x80, 0x01, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0xF0, 0x0F};
|
||||
|
||||
// Tiny Chirpy runner sprite: square head with two pill eyes + mouth, antenna, and long legs.
|
||||
// XBM, 2 bytes/row, LSB = leftmost pixel. Used by the Chirpy Runner game.
|
||||
#define chirpy_run_width 12
|
||||
#define chirpy_run_height 16
|
||||
static const uint8_t chirpy_run[] PROGMEM = {0x40, 0x00, 0x20, 0x00, 0x40, 0x00, 0xfc, 0x03, 0x04, 0x02, 0x94,
|
||||
0x02, 0x94, 0x02, 0x94, 0x02, 0x04, 0x02, 0xf4, 0x02, 0xfc, 0x03,
|
||||
0x90, 0x00, 0x90, 0x00, 0x90, 0x00, 0x90, 0x00, 0x08, 0x01};
|
||||
|
||||
#ifdef OLED_TINY
|
||||
#include "img/icon_small.xbm"
|
||||
#else
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
#include "main.h"
|
||||
|
||||
#include "RTC.h"
|
||||
#include "gps/RTC.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "RTC.h"
|
||||
#include "gps/RTC.h"
|
||||
|
||||
#include "GeoCoord.h"
|
||||
#include "NodeDB.h"
|
||||
|
||||
@@ -57,7 +57,6 @@ enum MenuAction {
|
||||
SET_REGION_TH,
|
||||
SET_REGION_LORA_24,
|
||||
SET_REGION_UA_433,
|
||||
SET_REGION_UA_868,
|
||||
SET_REGION_MY_433,
|
||||
SET_REGION_MY_919,
|
||||
SET_REGION_SG_923,
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
#include "MeshService.h"
|
||||
#include "MessageStore.h"
|
||||
#include "Power.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "airtime.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h"
|
||||
#include "graphics/niche/Utils/FlashData.h"
|
||||
#include "main.h"
|
||||
@@ -835,10 +835,6 @@ void InkHUD::MenuApplet::execute(MenuItem item)
|
||||
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_UA_433);
|
||||
break;
|
||||
|
||||
case SET_REGION_UA_868:
|
||||
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_UA_868);
|
||||
break;
|
||||
|
||||
case SET_REGION_MY_433:
|
||||
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_MY_433);
|
||||
break;
|
||||
@@ -1735,7 +1731,6 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
items.push_back(MenuItem("TH", MenuAction::SET_REGION_TH, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("LoRa 2.4", MenuAction::SET_REGION_LORA_24, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("UA 433", MenuAction::SET_REGION_UA_433, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("UA 868", MenuAction::SET_REGION_UA_868, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("MY 433", MenuAction::SET_REGION_MY_433, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("MY 919", MenuAction::SET_REGION_MY_919, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("SG 923", MenuAction::SET_REGION_SG_923, MenuPage::EXIT));
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#include "meshUtils.h"
|
||||
#include "modules/TextMessageModule.h"
|
||||
|
||||
#include "RTC.h"
|
||||
#include "gps/RTC.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "RTC.h"
|
||||
#include "gps/RTC.h"
|
||||
|
||||
#include "gps/GeoCoord.h"
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include "./RecentsListApplet.h"
|
||||
|
||||
#include "RTC.h"
|
||||
#include "gps/RTC.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include "./ThreadedMessageApplet.h"
|
||||
|
||||
#include "RTC.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "mesh/NodeDB.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
#include "MessageStore.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "buzz.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "modules/ExternalNotificationModule.h"
|
||||
#include "modules/TextMessageModule.h"
|
||||
#include "sleep.h"
|
||||
|
||||
+39
-20
@@ -79,6 +79,16 @@ static int axisZone(int value)
|
||||
return 0;
|
||||
}
|
||||
|
||||
void LinuxJoystick::emitEvent(input_broker_event event)
|
||||
{
|
||||
InputEvent e = {};
|
||||
e.inputEvent = event;
|
||||
e.source = this->_originName;
|
||||
e.kbchar = 0;
|
||||
// LOG_DEBUG("joystick: %s event %d", this->_originName, event);
|
||||
this->notifyObservers(&e);
|
||||
}
|
||||
|
||||
int32_t LinuxJoystick::runOnce()
|
||||
{
|
||||
if (firstTime) {
|
||||
@@ -107,8 +117,6 @@ int32_t LinuxJoystick::runOnce()
|
||||
if (nfds < 0) {
|
||||
perror("joystick: epoll_wait failed");
|
||||
return disable();
|
||||
} else if (nfds == 0) {
|
||||
return 50;
|
||||
}
|
||||
|
||||
for (int i = 0; i < nfds; i++) {
|
||||
@@ -117,42 +125,53 @@ int32_t LinuxJoystick::runOnce()
|
||||
if (rd < (signed int)sizeof(struct input_event))
|
||||
continue;
|
||||
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 code = evs[j].code;
|
||||
int value = evs[j].value;
|
||||
|
||||
if (type == EV_ABS) {
|
||||
// D-pad reports as ABS_X / ABS_Y with digital 0 / 127 / 255 values.
|
||||
// Emit exactly once when an axis crosses from center to an edge.
|
||||
// D-pad reports as ABS_X / ABS_Y with digital 0 / 127 / 255 values. Emit on the
|
||||
// transition to an edge and arm auto-repeat; a held direction repeats below.
|
||||
if (code == ABS_X) {
|
||||
int zone = axisZone(value);
|
||||
if (zone != axisZone(lastX) && zone != 0)
|
||||
e.inputEvent = (zone < 0) ? INPUT_BROKER_LEFT : INPUT_BROKER_RIGHT;
|
||||
lastX = value;
|
||||
if (zone != heldX) {
|
||||
heldX = zone;
|
||||
if (zone != 0) {
|
||||
emitEvent((zone < 0) ? INPUT_BROKER_LEFT : INPUT_BROKER_RIGHT);
|
||||
nextRepeatX = millis() + JOY_REPEAT_DELAY_MS;
|
||||
}
|
||||
}
|
||||
} else if (code == ABS_Y) {
|
||||
int zone = axisZone(value);
|
||||
if (zone != axisZone(lastY) && zone != 0)
|
||||
e.inputEvent = (zone < 0) ? INPUT_BROKER_UP : INPUT_BROKER_DOWN;
|
||||
lastY = value;
|
||||
if (zone != heldY) {
|
||||
heldY = zone;
|
||||
if (zone != 0) {
|
||||
emitEvent((zone < 0) ? INPUT_BROKER_UP : INPUT_BROKER_DOWN);
|
||||
nextRepeatY = millis() + JOY_REPEAT_DELAY_MS;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (type == EV_KEY && value == 1) {
|
||||
// Look up the configured action for this button; unmapped buttons are ignored.
|
||||
// Buttons fire once per press (no auto-repeat).
|
||||
auto mapped = buttonMap.find(code);
|
||||
if (mapped != buttonMap.end())
|
||||
e.inputEvent = mapped->second;
|
||||
}
|
||||
|
||||
if (e.inputEvent != INPUT_BROKER_NONE) {
|
||||
LOG_DEBUG("joystick: %s event %d", this->_originName, e.inputEvent);
|
||||
this->notifyObservers(&e);
|
||||
emitEvent(mapped->second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-repeat held D-pad directions. Signed comparison is wraparound-safe.
|
||||
uint32_t now = millis();
|
||||
if (heldX != 0 && (int32_t)(now - nextRepeatX) >= 0) {
|
||||
emitEvent((heldX < 0) ? INPUT_BROKER_LEFT : INPUT_BROKER_RIGHT);
|
||||
nextRepeatX = now + JOY_REPEAT_INTERVAL_MS;
|
||||
}
|
||||
if (heldY != 0 && (int32_t)(now - nextRepeatY) >= 0) {
|
||||
emitEvent((heldY < 0) ? INPUT_BROKER_UP : INPUT_BROKER_DOWN);
|
||||
nextRepeatY = now + JOY_REPEAT_INTERVAL_MS;
|
||||
}
|
||||
|
||||
return 50; // Poll every 50msec
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
#define JOY_AXIS_LOW 64 // below this -> "min" edge (0)
|
||||
#define JOY_AXIS_HIGH 192 // above this -> "max" edge (255)
|
||||
|
||||
// D-pad auto-repeat while a direction is held (typematic).
|
||||
#define JOY_REPEAT_DELAY_MS 400 // hold this long before repeats start
|
||||
#define JOY_REPEAT_INTERVAL_MS 150 // then repeat this often
|
||||
|
||||
class LinuxJoystick : public Observable<const InputEvent *>, public concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
@@ -27,10 +31,18 @@ class LinuxJoystick : public Observable<const InputEvent *>, public concurrency:
|
||||
void init(); // Registers this source with the InputBroker
|
||||
void deInit(); // Strictly for cleanly "rebooting" the binary on native
|
||||
|
||||
// Current held D-pad zone, updated the instant the axis moves (independent of the typematic
|
||||
// auto-repeat). -1 = left/up edge, 0 = centered, +1 = right/down edge. Lets a game poll for
|
||||
// smooth continuous control instead of waiting for the slow repeat events.
|
||||
int heldXZone() const { return heldX; }
|
||||
int heldYZone() const { return heldY; }
|
||||
|
||||
protected:
|
||||
virtual int32_t runOnce() override;
|
||||
|
||||
private:
|
||||
void emitEvent(input_broker_event event);
|
||||
|
||||
const char *_originName;
|
||||
bool firstTime = true;
|
||||
|
||||
@@ -42,10 +54,12 @@ class LinuxJoystick : public Observable<const InputEvent *>, public concurrency:
|
||||
int fd = -1;
|
||||
int epollfd = -1;
|
||||
|
||||
// Latch the last emitted axis position so a held direction fires exactly
|
||||
// once (on the transition away from center), not a continuous stream.
|
||||
int lastX = JOY_AXIS_CENTER;
|
||||
int lastY = JOY_AXIS_CENTER;
|
||||
// D-pad auto-repeat state: currently held zone per axis (-1 / 0 / +1) and the
|
||||
// next millis() timestamp at which to re-emit while the direction is held.
|
||||
int heldX = 0;
|
||||
int heldY = 0;
|
||||
uint32_t nextRepeatX = 0;
|
||||
uint32_t nextRepeatY = 0;
|
||||
};
|
||||
extern LinuxJoystick *aLinuxJoystick;
|
||||
#endif
|
||||
|
||||
+5
-1
@@ -22,13 +22,13 @@
|
||||
|
||||
#include "FSCommon.h"
|
||||
#include "Power.h"
|
||||
#include "RTC.h"
|
||||
#include "SPILock.h"
|
||||
#include "Throttle.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "concurrency/Periodic.h"
|
||||
#include "detect/ScanI2C.h"
|
||||
#include "error.h"
|
||||
#include "gps/RTC.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_I2C
|
||||
#include "detect/ScanI2CConsumer.h"
|
||||
@@ -816,6 +816,10 @@ void setup()
|
||||
rp2040Setup();
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_STM32WL
|
||||
stm32wlSetup();
|
||||
#endif
|
||||
|
||||
// We do this as early as possible because this loads preferences from flash
|
||||
// but we need to do this after main cpu init (esp32setup), because we need the random seed set
|
||||
nodeDB = new NodeDB;
|
||||
|
||||
+2
-1
@@ -113,7 +113,8 @@ extern bool runASAP;
|
||||
|
||||
extern bool pauseBluetoothLogging;
|
||||
|
||||
void nrf52Setup(), esp32Setup(), nrf52Loop(), esp32Loop(), rp2040Setup(), rp2040Loop(), clearBonds(), enterDfuMode();
|
||||
void nrf52Setup(), esp32Setup(), nrf52Loop(), esp32Loop(), rp2040Setup(), rp2040Loop(), clearBonds(), enterDfuMode(),
|
||||
stm32wlSetup();
|
||||
#ifdef ARCH_ESP32
|
||||
void esp32ReleaseBluetoothMemoryIfUnused();
|
||||
#endif
|
||||
|
||||
@@ -516,7 +516,7 @@ bool Channels::hasDefaultChannel()
|
||||
*/
|
||||
bool Channels::decryptForHash(ChannelIndex chIndex, ChannelHash channelHash)
|
||||
{
|
||||
if (chIndex > getNumChannels() || getHash(chIndex) != channelHash) {
|
||||
if (chIndex >= getNumChannels() || getHash(chIndex) != channelHash) {
|
||||
// LOG_DEBUG("Skip channel %d (hash %x) due to invalid hash/index, want=%x", chIndex, getHash(chIndex),
|
||||
// channelHash);
|
||||
return false;
|
||||
|
||||
@@ -224,7 +224,11 @@ bool CryptoEngine::encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtas
|
||||
uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut)
|
||||
{
|
||||
uint8_t *auth;
|
||||
long extraNonceTmp = random();
|
||||
// The extra nonce must be unpredictable: use the hardware RNG, falling back to the
|
||||
// seeded CSPRNG only when no hardware source is available.
|
||||
uint32_t extraNonceTmp;
|
||||
if (!HardwareRNG::fill((uint8_t *)&extraNonceTmp, sizeof(extraNonceTmp)))
|
||||
CryptRNG.rand((uint8_t *)&extraNonceTmp, sizeof(extraNonceTmp));
|
||||
auth = bytesOut + numBytes;
|
||||
memcpy((uint8_t *)(auth + 8), &extraNonceTmp,
|
||||
sizeof(uint32_t)); // do not use dereference on potential non aligned pointers : *extraNonce = extraNonceTmp;
|
||||
@@ -341,6 +345,32 @@ bool CryptoEngine::setDHPublicKey(uint8_t *pubKey)
|
||||
return true;
|
||||
}
|
||||
|
||||
void CryptoEngine::setPendingPublicKey(uint32_t node, const uint8_t *key)
|
||||
{
|
||||
concurrency::LockGuard g(&pendingKeyLock);
|
||||
pendingKeyVerificationNode = node;
|
||||
memcpy(pendingKeyVerificationPublicKey, key, 32);
|
||||
hasPendingKeyVerificationKey = true;
|
||||
}
|
||||
|
||||
void CryptoEngine::clearPendingPublicKey()
|
||||
{
|
||||
concurrency::LockGuard g(&pendingKeyLock);
|
||||
pendingKeyVerificationNode = 0;
|
||||
memset(pendingKeyVerificationPublicKey, 0, 32);
|
||||
hasPendingKeyVerificationKey = false;
|
||||
}
|
||||
|
||||
bool CryptoEngine::getPendingPublicKey(uint32_t node, meshtastic_NodeInfoLite_public_key_t &out)
|
||||
{
|
||||
concurrency::LockGuard g(&pendingKeyLock);
|
||||
if (!hasPendingKeyVerificationKey || node == 0 || node != pendingKeyVerificationNode)
|
||||
return false;
|
||||
out.size = 32;
|
||||
memcpy(out.bytes, pendingKeyVerificationPublicKey, 32);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
concurrency::Lock *cryptLock;
|
||||
|
||||
|
||||
@@ -59,6 +59,17 @@ class CryptoEngine
|
||||
virtual bool setDHPublicKey(uint8_t *publicKey);
|
||||
virtual void hash(uint8_t *bytes, size_t numBytes);
|
||||
|
||||
// Temporary holder for a peer's not-yet-verified public key, learned in-band during an
|
||||
// in-progress key-verification handshake before it is committed to NodeDB. Lets the Router
|
||||
// run the DH handshake to encode/decode the follow-on PKI packet. Single slot is enough:
|
||||
// only one verification runs at a time. Discarded when the handshake ends (resetToIdle).
|
||||
// Internally guarded by pendingKeyLock, not cryptLock: the Router calls the getter while
|
||||
// already holding the non-recursive cryptLock; KeyVerificationModule writes from elsewhere.
|
||||
void setPendingPublicKey(uint32_t node, const uint8_t *key);
|
||||
void clearPendingPublicKey();
|
||||
// Fills `out` (size set to 32) and returns true iff a pending key is held for `node`.
|
||||
bool getPendingPublicKey(uint32_t node, meshtastic_NodeInfoLite_public_key_t &out);
|
||||
|
||||
virtual void aesSetKey(const uint8_t *key, size_t key_len);
|
||||
|
||||
virtual void aesEncrypt(uint8_t *in, uint8_t *out);
|
||||
@@ -94,6 +105,10 @@ class CryptoEngine
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||
uint8_t shared_key[32] = {0};
|
||||
uint8_t private_key[32] = {0};
|
||||
uint32_t pendingKeyVerificationNode = 0;
|
||||
uint8_t pendingKeyVerificationPublicKey[32] = {0};
|
||||
bool hasPendingKeyVerificationKey = false;
|
||||
concurrency::Lock pendingKeyLock;
|
||||
#if !(MESHTASTIC_EXCLUDE_XEDDSA)
|
||||
uint8_t xeddsa_public_key[32] = {0};
|
||||
uint8_t xeddsa_private_key[32] = {0};
|
||||
|
||||
@@ -11,12 +11,13 @@
|
||||
#include "NodeDB.h"
|
||||
#include "Power.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "TypeConversions.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "graphics/draw/MessageRenderer.h"
|
||||
#include "main.h"
|
||||
#include "mesh-pb-constants.h"
|
||||
#include "meshUtils.h"
|
||||
#include "modules/AdminModule.h"
|
||||
#include "modules/NodeInfoModule.h"
|
||||
#include "modules/PositionModule.h"
|
||||
#include "modules/RoutingModule.h"
|
||||
@@ -63,6 +64,7 @@ Allocator<meshtastic_ClientNotification> &clientNotificationPool = staticClientN
|
||||
|
||||
Allocator<meshtastic_QueueStatus> &queueStatusPool = staticQueueStatusPool;
|
||||
|
||||
#include "PositionPrecision.h"
|
||||
#include "Router.h"
|
||||
|
||||
MeshService::MeshService()
|
||||
@@ -173,6 +175,51 @@ NodeNum MeshService::getNodenumFromRequestId(uint32_t request_id)
|
||||
return nodenum;
|
||||
}
|
||||
|
||||
#if MESHTASTIC_ENABLE_FRAME_INJECTION
|
||||
// Deliver a client-supplied frame into the receive pipeline as if it arrived off the LoRa chip. Mirrors
|
||||
// the portduino SimRadio SIMULATOR_APP unwrap so the same host wire format works on real hardware: the
|
||||
// frame rides inside a Compressed envelope wrapped in a MeshPacket that carries from/to/id/channel.
|
||||
// Compressed.portnum == UNKNOWN_APP -> Compressed.data is verbatim ciphertext, decrypted as if off-air
|
||||
// otherwise -> Compressed.data is the plaintext payload for Compressed.portnum
|
||||
void MeshService::injectAsReceived(meshtastic_MeshPacket &p)
|
||||
{
|
||||
meshtastic_Compressed scratch;
|
||||
if (p.which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
|
||||
memset(&scratch, 0, sizeof(scratch));
|
||||
if (pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_Compressed_msg, &scratch)) {
|
||||
if (scratch.portnum == meshtastic_PortNum_UNKNOWN_APP) {
|
||||
p.which_payload_variant = meshtastic_MeshPacket_encrypted_tag;
|
||||
memcpy(p.encrypted.bytes, scratch.data.bytes, scratch.data.size);
|
||||
p.encrypted.size = scratch.data.size;
|
||||
} else {
|
||||
memcpy(&p.decoded.payload, &scratch.data, sizeof(scratch.data));
|
||||
p.decoded.portnum = scratch.portnum;
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR("inject: could not decode Compressed envelope, dropping");
|
||||
return;
|
||||
}
|
||||
}
|
||||
// The real RX path (RadioLibInterface::handleReceiveInterrupt) drops sender==0; mirror it so injection
|
||||
// behaves identically to an over-the-air frame.
|
||||
if (p.from == 0) {
|
||||
LOG_WARN("inject: dropping frame with from==0 (matches real LoRa RX)");
|
||||
return;
|
||||
}
|
||||
meshtastic_MeshPacket *mp = packetPool.allocCopy(p);
|
||||
if (!mp)
|
||||
return;
|
||||
if (mp->rx_snr == 0) // plausible synthetic link metadata unless the caller set it
|
||||
mp->rx_snr = 8;
|
||||
if (mp->rx_rssi == 0)
|
||||
mp->rx_rssi = -40;
|
||||
mp->rx_time = getValidTime(RTCQualityFromNet);
|
||||
LOG_INFO("inject: RX from=0x%08x to=0x%08x id=0x%08x ch=%d %s", mp->from, mp->to, mp->id, mp->channel,
|
||||
mp->which_payload_variant == meshtastic_MeshPacket_encrypted_tag ? "encrypted" : "decoded");
|
||||
router->enqueueReceivedMessage(mp);
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Given a ToRadio buffer parse it and properly handle it (setup radio, owner or send packet into the mesh)
|
||||
* Called by PhoneAPI.handleToRadio. Note: p is a scratch buffer, this function is allowed to write to it but it can not keep a
|
||||
@@ -186,6 +233,16 @@ void MeshService::handleToRadio(meshtastic_MeshPacket &p)
|
||||
SimRadio::instance->unpackAndReceive(p);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#if MESHTASTIC_ENABLE_FRAME_INJECTION
|
||||
// Real-hardware analog of the SimRadio path above: deliver a client-supplied frame into the RX
|
||||
// pipeline exactly as if it had arrived off the LoRa chip. Reached before the p.from=0 line below,
|
||||
// so an injected sender is preserved. Build-flag gated (off by default) - it lets anything with a
|
||||
// wired connection forge over-the-air traffic, so it must never ship enabled.
|
||||
if (p.which_payload_variant == meshtastic_MeshPacket_decoded_tag && p.decoded.portnum == meshtastic_PortNum_SIMULATOR_APP) {
|
||||
injectAsReceived(p);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
p.from = 0; // We don't let clients assign nodenums to their sent messages
|
||||
p.next_hop = NO_NEXT_HOP_PREFERENCE; // We don't let clients assign next_hop to their sent messages
|
||||
@@ -203,6 +260,14 @@ void MeshService::handleToRadio(meshtastic_MeshPacket &p)
|
||||
const StoredMessage &sm = messageStore.addFromPacket(p);
|
||||
graphics::MessageRenderer::handleNewMessage(nullptr, sm, p); // notify UI
|
||||
})
|
||||
#if !MESHTASTIC_EXCLUDE_ADMIN
|
||||
// Note admin requests on their way out: AdminModule only accepts a response from a remote we
|
||||
// actually asked. Runs before encryption, while the payload is still readable.
|
||||
if (adminModule && p.which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
|
||||
p.decoded.portnum == meshtastic_PortNum_ADMIN_APP)
|
||||
adminModule->noteOutgoingAdminRequest(p);
|
||||
#endif
|
||||
|
||||
// Send the packet into the mesh
|
||||
DEBUG_HEAP_BEFORE;
|
||||
auto a = packetPool.allocCopy(p);
|
||||
@@ -293,8 +358,31 @@ bool MeshService::trySendPosition(NodeNum dest, bool wantReplies)
|
||||
LOG_DEBUG("Skip position ping; no fresh position since boot");
|
||||
return false;
|
||||
}
|
||||
LOG_INFO("Send position ping to 0x%08x, wantReplies=%d, channel=%d", dest, wantReplies, node->channel);
|
||||
positionModule->sendOurPosition(dest, wantReplies, node->channel);
|
||||
// Prefer the node's current channel, but fall back to the first channel with
|
||||
// position enabled (matching PositionModule::sendOurPosition() behavior).
|
||||
uint8_t sendChan = node->channel;
|
||||
if (getPositionPrecisionForChannel(sendChan) == 0) {
|
||||
bool found = false;
|
||||
for (uint8_t ch = 0; ch < 8; ++ch) {
|
||||
if (getPositionPrecisionForChannel(ch) != 0) {
|
||||
sendChan = ch;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
// No channel with position enabled: fall back to sending nodeinfo, as before.
|
||||
if (nodeInfoModule) {
|
||||
LOG_INFO(
|
||||
"No channel with position enabled; sending nodeinfo instead to 0x%08x, wantReplies=%d, channel=%d",
|
||||
dest, wantReplies, node->channel);
|
||||
nodeInfoModule->sendOurNodeInfo(dest, wantReplies, node->channel);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
LOG_INFO("Send position ping to 0x%08x, wantReplies=%d, channel=%d", dest, wantReplies, sendChan);
|
||||
positionModule->sendOurPosition(dest, wantReplies, sendChan);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -159,6 +159,12 @@ class MeshService
|
||||
*/
|
||||
void handleToRadio(meshtastic_MeshPacket &p);
|
||||
|
||||
#if MESHTASTIC_ENABLE_FRAME_INJECTION
|
||||
/// Test/debug seam (build-flag gated, off by default): deliver a client-supplied frame into the
|
||||
/// receive pipeline as if it had arrived off the LoRa chip. See the definition for the wire format.
|
||||
void injectAsReceived(meshtastic_MeshPacket &p);
|
||||
#endif
|
||||
|
||||
/** The radioConfig object just changed, call this to force the hw to change to the new settings
|
||||
* @return true if client devices should be sent a new set of radio configs
|
||||
*/
|
||||
|
||||
+53
-63
@@ -13,7 +13,6 @@
|
||||
#include "NodeDB.h"
|
||||
#include "PacketHistory.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "RadioInterface.h"
|
||||
#include "Router.h"
|
||||
#include "SPILock.h"
|
||||
@@ -21,12 +20,14 @@
|
||||
#include "TransmitHistory.h"
|
||||
#include "TypeConversions.h"
|
||||
#include "error.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "main.h"
|
||||
#include "memory/MemAudit.h"
|
||||
#include "mesh-pb-constants.h"
|
||||
#include "mesh/generated/meshtastic/deviceonly_legacy.pb.h"
|
||||
#include "meshUtils.h"
|
||||
#include "modules/NeighborInfoModule.h"
|
||||
#include "target_specific.h"
|
||||
#if HAS_VARIABLE_HOPS
|
||||
#include "modules/HopScalingModule.h"
|
||||
#endif
|
||||
@@ -50,11 +51,7 @@
|
||||
#include "SPILock.h"
|
||||
#include "modules/StoreForwardModule.h"
|
||||
#include <Preferences.h>
|
||||
#include <esp_efuse.h>
|
||||
#include <esp_efuse_table.h>
|
||||
#include <nvs_flash.h>
|
||||
#include <soc/efuse_reg.h>
|
||||
#include <soc/soc.h>
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_PORTDUINO
|
||||
@@ -372,8 +369,7 @@ void NodeDB::disarmNodeDatabaseDecodeTargets()
|
||||
*/
|
||||
uint32_t radioGeneration;
|
||||
|
||||
// FIXME - move this somewhere else
|
||||
extern void getMacAddr(uint8_t *dmac);
|
||||
// getMacAddr() and getDeviceId() are the per-architecture hooks declared in target_specific.h.
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -410,53 +406,13 @@ NodeDB::NodeDB()
|
||||
uint32_t channelFileCRC = crc32Buffer(&channelFile, sizeof(channelFile));
|
||||
|
||||
int saveWhat = 0;
|
||||
// Get device unique id
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6)
|
||||
uint32_t unique_id[4];
|
||||
// ESP32 factory burns a unique id in efuse for S2+ series and evidently C3+ series
|
||||
// This is used for HMACs in the esp-rainmaker AIOT platform and seems to be a good choice for us
|
||||
esp_err_t err = esp_efuse_read_field_blob(ESP_EFUSE_OPTIONAL_UNIQUE_ID, unique_id, sizeof(unique_id) * 8);
|
||||
if (err == ESP_OK) {
|
||||
memcpy(myNodeInfo.device_id.bytes, unique_id, sizeof(unique_id));
|
||||
myNodeInfo.device_id.size = 16;
|
||||
} else {
|
||||
LOG_WARN("Failed to read unique id from efuse");
|
||||
// Re-read the device id from silicon each boot via the per-arch getDeviceId(); clear the
|
||||
// disk-loaded value first so a failed/empty derivation leaves it unset rather than stale.
|
||||
myNodeInfo.device_id.size = 0;
|
||||
memset(myNodeInfo.device_id.bytes, 0, sizeof(myNodeInfo.device_id.bytes));
|
||||
if (getDeviceId(myNodeInfo.device_id.bytes)) {
|
||||
myNodeInfo.device_id.size = sizeof(myNodeInfo.device_id.bytes);
|
||||
}
|
||||
#elif defined(ARCH_NRF54L15)
|
||||
// nRF54L15: DEVICEID is under FICR->INFO sub-struct (not top-level as on nRF52)
|
||||
uint64_t device_id_start = ((uint64_t)NRF_FICR->INFO.DEVICEID[1] << 32) | NRF_FICR->INFO.DEVICEID[0];
|
||||
uint64_t device_id_end = ((uint64_t)NRF_FICR->DEVICEADDR[1] << 32) | NRF_FICR->DEVICEADDR[0];
|
||||
memcpy(myNodeInfo.device_id.bytes, &device_id_start, sizeof(device_id_start));
|
||||
memcpy(myNodeInfo.device_id.bytes + sizeof(device_id_start), &device_id_end, sizeof(device_id_end));
|
||||
myNodeInfo.device_id.size = 16;
|
||||
#elif defined(ARCH_NRF52)
|
||||
// Nordic applies a FIPS compliant Random ID to each chip at the factory
|
||||
// We concatenate the device address to the Random ID to create a unique ID for now
|
||||
// This will likely utilize a crypto module in the future
|
||||
uint64_t device_id_start = ((uint64_t)NRF_FICR->DEVICEID[1] << 32) | NRF_FICR->DEVICEID[0];
|
||||
uint64_t device_id_end = ((uint64_t)NRF_FICR->DEVICEADDR[1] << 32) | NRF_FICR->DEVICEADDR[0];
|
||||
memcpy(myNodeInfo.device_id.bytes, &device_id_start, sizeof(device_id_start));
|
||||
memcpy(myNodeInfo.device_id.bytes + sizeof(device_id_start), &device_id_end, sizeof(device_id_end));
|
||||
myNodeInfo.device_id.size = 16;
|
||||
// Uncomment below to print the device id
|
||||
#elif ARCH_PORTDUINO
|
||||
if (portduino_config.has_device_id) {
|
||||
memcpy(myNodeInfo.device_id.bytes, portduino_config.device_id, 16);
|
||||
myNodeInfo.device_id.size = 16;
|
||||
}
|
||||
#else
|
||||
// FIXME - implement for other platforms
|
||||
#endif
|
||||
|
||||
// if (myNodeInfo.device_id.size == 16) {
|
||||
// std::string deviceIdHex;
|
||||
// for (size_t i = 0; i < myNodeInfo.device_id.size; ++i) {
|
||||
// char buf[3];
|
||||
// snprintf(buf, sizeof(buf), "%02X", myNodeInfo.device_id.bytes[i]);
|
||||
// deviceIdHex += buf;
|
||||
// }
|
||||
// LOG_DEBUG("Device ID (HEX): %s", deviceIdHex.c_str());
|
||||
// }
|
||||
|
||||
// likewise - we always want the app requirements to come from the running appload
|
||||
myNodeInfo.min_app_version = 30200; // format is Mmmss (where M is 1+the numeric major number. i.e. 30200 means 2.2.00
|
||||
@@ -522,6 +478,12 @@ NodeDB::NodeDB()
|
||||
LOG_DEBUG("Number of Device Reboots: %d", myNodeInfo.reboot_count);
|
||||
#endif
|
||||
|
||||
// UA_868 is obsolete; migrate to EU_868 before resetRadioConfig() below validates the region.
|
||||
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UA_868) {
|
||||
LOG_INFO("UA_868 region is obsolete, migrating saved config to EU_868");
|
||||
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
||||
}
|
||||
|
||||
resetRadioConfig(); // If bogus settings got saved, then fix them
|
||||
// nodeDB->LOG_DEBUG("region=%d, NODENUM=0x%x, dbsize=%d", config.lora.region, myNodeInfo.my_node_num, numMeshNodes);
|
||||
|
||||
@@ -1564,16 +1526,34 @@ void NodeDB::initModuleConfigIntervals()
|
||||
moduleConfig.telemetry.device_update_interval = MAX_INTERVAL;
|
||||
#endif
|
||||
|
||||
#ifdef USERPREFS_CONFIG_ENVIRONMENT_MEASUREMENT_ENABLED
|
||||
moduleConfig.telemetry.environment_measurement_enabled = USERPREFS_CONFIG_ENVIRONMENT_MEASUREMENT_ENABLED;
|
||||
#endif
|
||||
|
||||
#ifdef USERPREFS_CONFIG_ENV_TELEM_UPDATE_INTERVAL
|
||||
moduleConfig.telemetry.environment_update_interval = USERPREFS_CONFIG_ENV_TELEM_UPDATE_INTERVAL;
|
||||
#else
|
||||
moduleConfig.telemetry.environment_update_interval = 0;
|
||||
#endif
|
||||
|
||||
#ifdef USERPREFS_CONFIG_ENVIRONMENT_MEASUREMENT_ENABLED
|
||||
moduleConfig.telemetry.environment_measurement_enabled = USERPREFS_CONFIG_ENVIRONMENT_MEASUREMENT_ENABLED;
|
||||
#ifdef USERPREFS_CONFIG_ENV_SCREEN_SCREEN_ENABLED
|
||||
moduleConfig.telemetry.environment_screen_enabled = USERPREFS_CONFIG_ENV_SCREEN_SCREEN_ENABLED;
|
||||
#endif
|
||||
|
||||
#ifdef USERPREFS_CONFIG_AQ_TELEM_UPDATE_INTERVAL
|
||||
moduleConfig.telemetry.air_quality_interval = USERPREFS_CONFIG_AQ_TELEM_UPDATE_INTERVAL;
|
||||
#else
|
||||
moduleConfig.telemetry.air_quality_interval = 0;
|
||||
#endif
|
||||
|
||||
#ifdef USERPREFS_CONFIG_AQ_MEASUREMENT_ENABLED
|
||||
moduleConfig.telemetry.air_quality_enabled = USERPREFS_CONFIG_AQ_MEASUREMENT_ENABLED;
|
||||
#endif
|
||||
|
||||
#ifdef USERPREFS_CONFIG_AQ_SCREEN_ENABLED
|
||||
moduleConfig.telemetry.air_quality_screen_enabled = USERPREFS_CONFIG_AQ_SCREEN_ENABLED;
|
||||
#endif
|
||||
|
||||
moduleConfig.telemetry.power_update_interval = 0;
|
||||
moduleConfig.telemetry.health_update_interval = 0;
|
||||
moduleConfig.neighbor_info.update_interval = 0;
|
||||
@@ -1635,10 +1615,16 @@ void NodeDB::removeNodeByNum(NodeNum nodeNum)
|
||||
removed++;
|
||||
}
|
||||
numMeshNodes -= removed;
|
||||
std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.begin() + numMeshNodes + 1,
|
||||
meshtastic_NodeInfoLite());
|
||||
if (removed)
|
||||
eraseNodeSatellites(nodeNum);
|
||||
if (removed) {
|
||||
// Clear exactly the slots compaction vacated. Sizing this from `removed` (rather than a
|
||||
// fixed one) keeps it inside the vector when nothing matched and the store is full.
|
||||
const size_t first = numMeshNodes;
|
||||
const size_t last = std::min(first + removed, nodeDatabase.nodes.size());
|
||||
std::fill(nodeDatabase.nodes.begin() + first, nodeDatabase.nodes.begin() + last, meshtastic_NodeInfoLite());
|
||||
}
|
||||
// Drop the node's satellite stores and warm-tier copy regardless of which tier it lived in, so
|
||||
// an explicit removal fully forgets it.
|
||||
eraseNodeSatellites(nodeNum);
|
||||
#if WARM_NODE_COUNT > 0
|
||||
// Explicit user removal: don't let the warm tier resurrect the node
|
||||
warmStore.remove(nodeNum);
|
||||
@@ -1896,7 +1882,8 @@ void NodeDB::cleanupMeshDB()
|
||||
// Keep any key we learned (e.g. via a DM before the NodeInfo
|
||||
// exchange completed) rather than losing it with the purge.
|
||||
if (n.public_key.size == 32)
|
||||
warmStore.absorb(gone, n.last_heard, n.public_key.bytes, n.role, warmProtectedCategory(n));
|
||||
warmStore.absorb(gone, n.last_heard, n.public_key.bytes, n.role, warmProtectedCategory(n),
|
||||
nodeInfoLiteHasXeddsaSigned(&n));
|
||||
#endif
|
||||
|
||||
eraseNodeSatellites(gone);
|
||||
@@ -2080,7 +2067,7 @@ void NodeDB::demoteOldestHotNodesToWarm()
|
||||
// Keep the public key if we have one (40 B warm record); keyless nodes
|
||||
// still get a placeholder so re-admission restores last_heard.
|
||||
warmStore.absorb(n.num, n.last_heard, n.public_key.size > 0 ? n.public_key.bytes : nullptr, n.role,
|
||||
warmProtectedCategory(n));
|
||||
warmProtectedCategory(n), nodeInfoLiteHasXeddsaSigned(&n));
|
||||
// Demotion drops the node from the header table, so drop its satellites
|
||||
// too (the eviction chokepoint) - they'd otherwise orphan until the next
|
||||
// enforceSatelliteCaps pass.
|
||||
@@ -3772,7 +3759,7 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
|
||||
// Demote to the warm tier so the identity (and crucially the
|
||||
// PKI key) outlives the hot-store slot.
|
||||
warmStore.absorb(evicted.num, evicted.last_heard, evicted.public_key.size == 32 ? evicted.public_key.bytes : NULL,
|
||||
evicted.role, warmProtectedCategory(evicted));
|
||||
evicted.role, warmProtectedCategory(evicted), nodeInfoLiteHasXeddsaSigned(&evicted));
|
||||
#endif
|
||||
eraseNodeSatellites(evicted.num);
|
||||
// Shove the remaining nodes down the chain
|
||||
@@ -3801,10 +3788,13 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
|
||||
// Re-admission: restore what the warm tier kept for this node
|
||||
WarmNodeEntry warm;
|
||||
if (warmStore.take(n, warm)) {
|
||||
lite->last_heard = warmTimeOf(warm); // mask off the stolen role/protected metadata bits
|
||||
lite->last_heard = warmTimeOf(warm); // mask off the stolen metadata bits
|
||||
// Restore the role the warm tier cached, so re-admission isn't stuck at CLIENT
|
||||
// until the next NodeInfo arrives.
|
||||
lite->role = static_cast<meshtastic_Config_DeviceConfig_Role>(warmRoleOf(warm));
|
||||
// Restore the signer bit too: it is learned from verified traffic, not from
|
||||
// NodeInfo, so a round trip through the warm tier must not relearn it from zero.
|
||||
nodeInfoLiteSetBit(lite, NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_MASK, warmSignerOf(warm));
|
||||
if (!memfll(warm.public_key, 0, sizeof(warm.public_key))) {
|
||||
lite->public_key.size = 32;
|
||||
memcpy(lite->public_key.bytes, warm.public_key, 32);
|
||||
|
||||
@@ -1,253 +0,0 @@
|
||||
#include "PacketCache.h"
|
||||
#include "Router.h"
|
||||
|
||||
PacketCache packetCache{};
|
||||
|
||||
/**
|
||||
* Allocate a new cache entry and copy the packet header and payload into it
|
||||
*/
|
||||
PacketCacheEntry *PacketCache::cache(const meshtastic_MeshPacket *p, bool preserveMetadata)
|
||||
{
|
||||
size_t payload_size =
|
||||
(p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag) ? p->encrypted.size : p->decoded.payload.size;
|
||||
PacketCacheEntry *e = (PacketCacheEntry *)malloc(sizeof(PacketCacheEntry) + payload_size +
|
||||
(preserveMetadata ? sizeof(PacketCacheMetadata) : 0));
|
||||
if (!e) {
|
||||
LOG_ERROR("Unable to allocate memory for packet cache entry");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
*e = {};
|
||||
e->header.from = p->from;
|
||||
e->header.to = p->to;
|
||||
e->header.id = p->id;
|
||||
e->header.channel = p->channel;
|
||||
e->header.next_hop = p->next_hop;
|
||||
e->header.relay_node = p->relay_node;
|
||||
e->header.flags = (p->hop_limit & PACKET_FLAGS_HOP_LIMIT_MASK) | (p->want_ack ? PACKET_FLAGS_WANT_ACK_MASK : 0) |
|
||||
(p->via_mqtt ? PACKET_FLAGS_VIA_MQTT_MASK : 0) |
|
||||
((p->hop_start << PACKET_FLAGS_HOP_START_SHIFT) & PACKET_FLAGS_HOP_START_MASK);
|
||||
|
||||
PacketCacheMetadata m{};
|
||||
if (preserveMetadata) {
|
||||
e->has_metadata = true;
|
||||
m.rx_rssi = (uint8_t)(p->rx_rssi + 200);
|
||||
m.rx_snr = (uint8_t)((p->rx_snr + 30.0f) / 0.25f);
|
||||
m.rx_time = p->rx_time;
|
||||
m.transport_mechanism = p->transport_mechanism;
|
||||
m.priority = p->priority;
|
||||
}
|
||||
if (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag) {
|
||||
e->encrypted = true;
|
||||
e->payload_len = p->encrypted.size;
|
||||
memcpy(((unsigned char *)e) + sizeof(PacketCacheEntry), p->encrypted.bytes, p->encrypted.size);
|
||||
} else if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
|
||||
e->encrypted = false;
|
||||
if (preserveMetadata) {
|
||||
m.portnum = p->decoded.portnum;
|
||||
m.want_response = p->decoded.want_response;
|
||||
m.emoji = p->decoded.emoji;
|
||||
m.bitfield = p->decoded.bitfield;
|
||||
if (p->decoded.reply_id)
|
||||
m.reply_id = p->decoded.reply_id;
|
||||
else if (p->decoded.request_id)
|
||||
m.request_id = p->decoded.request_id;
|
||||
}
|
||||
e->payload_len = p->decoded.payload.size;
|
||||
memcpy(((unsigned char *)e) + sizeof(PacketCacheEntry), p->decoded.payload.bytes, p->decoded.payload.size);
|
||||
} else {
|
||||
LOG_ERROR("Unable to cache packet with unknown payload type %d", p->which_payload_variant);
|
||||
free(e);
|
||||
return NULL;
|
||||
}
|
||||
if (preserveMetadata)
|
||||
memcpy(((unsigned char *)e) + sizeof(PacketCacheEntry) + e->payload_len, &m, sizeof(m));
|
||||
|
||||
size += sizeof(PacketCacheEntry) + e->payload_len + (e->has_metadata ? sizeof(PacketCacheMetadata) : 0);
|
||||
insert(e);
|
||||
return e;
|
||||
};
|
||||
|
||||
/**
|
||||
* Dump a list of packets into the provided buffer
|
||||
*/
|
||||
void PacketCache::dump(void *dest, const PacketCacheEntry **entries, size_t num_entries)
|
||||
{
|
||||
unsigned char *pos = (unsigned char *)dest;
|
||||
for (size_t i = 0; i < num_entries; i++) {
|
||||
size_t entry_len =
|
||||
sizeof(PacketCacheEntry) + entries[i]->payload_len + (entries[i]->has_metadata ? sizeof(PacketCacheMetadata) : 0);
|
||||
memcpy(pos, entries[i], entry_len);
|
||||
pos += entry_len;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the length of buffer needed to dump the specified entries
|
||||
*/
|
||||
size_t PacketCache::dumpSize(const PacketCacheEntry **entries, size_t num_entries)
|
||||
{
|
||||
size_t total_size = 0;
|
||||
for (size_t i = 0; i < num_entries; i++) {
|
||||
total_size += sizeof(PacketCacheEntry) + entries[i]->payload_len;
|
||||
if (entries[i]->has_metadata)
|
||||
total_size += sizeof(PacketCacheMetadata);
|
||||
}
|
||||
return total_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a packet in the cache
|
||||
*/
|
||||
PacketCacheEntry *PacketCache::find(NodeNum from, PacketId id)
|
||||
{
|
||||
uint16_t h = PACKET_HASH(from, id);
|
||||
PacketCacheEntry *e = buckets[PACKET_CACHE_BUCKET(h)];
|
||||
while (e) {
|
||||
if (e->header.from == from && e->header.id == id)
|
||||
return e;
|
||||
e = e->next;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a packet in the cache by its hash
|
||||
*/
|
||||
PacketCacheEntry *PacketCache::find(PacketHash h)
|
||||
{
|
||||
PacketCacheEntry *e = buckets[PACKET_CACHE_BUCKET(h)];
|
||||
while (e) {
|
||||
if (PACKET_HASH(e->header.from, e->header.id) == h)
|
||||
return e;
|
||||
e = e->next;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a list of packets from the provided buffer
|
||||
*/
|
||||
bool PacketCache::load(void *src, PacketCacheEntry **entries, size_t num_entries)
|
||||
{
|
||||
memset(entries, 0, sizeof(PacketCacheEntry *) * num_entries);
|
||||
unsigned char *pos = (unsigned char *)src;
|
||||
for (size_t i = 0; i < num_entries; i++) {
|
||||
PacketCacheEntry e{};
|
||||
memcpy(&e, pos, sizeof(PacketCacheEntry));
|
||||
size_t entry_len = sizeof(PacketCacheEntry) + e.payload_len + (e.has_metadata ? sizeof(PacketCacheMetadata) : 0);
|
||||
entries[i] = (PacketCacheEntry *)malloc(entry_len);
|
||||
size += entry_len;
|
||||
if (!entries[i]) {
|
||||
LOG_ERROR("Unable to allocate memory for packet cache entry");
|
||||
for (size_t j = 0; j < i; j++) {
|
||||
size -= sizeof(PacketCacheEntry) + entries[j]->payload_len +
|
||||
(entries[j]->has_metadata ? sizeof(PacketCacheMetadata) : 0);
|
||||
free(entries[j]);
|
||||
entries[j] = NULL;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
memcpy(entries[i], pos, entry_len);
|
||||
pos += entry_len;
|
||||
}
|
||||
for (size_t i = 0; i < num_entries; i++)
|
||||
insert(entries[i]);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the cached packet into the provided MeshPacket structure
|
||||
*/
|
||||
void PacketCache::rehydrate(const PacketCacheEntry *e, meshtastic_MeshPacket *p)
|
||||
{
|
||||
if (!e || !p)
|
||||
return;
|
||||
|
||||
*p = {};
|
||||
p->from = e->header.from;
|
||||
p->to = e->header.to;
|
||||
p->id = e->header.id;
|
||||
p->channel = e->header.channel;
|
||||
p->next_hop = e->header.next_hop;
|
||||
p->relay_node = e->header.relay_node;
|
||||
p->hop_limit = e->header.flags & PACKET_FLAGS_HOP_LIMIT_MASK;
|
||||
p->want_ack = !!(e->header.flags & PACKET_FLAGS_WANT_ACK_MASK);
|
||||
p->via_mqtt = !!(e->header.flags & PACKET_FLAGS_VIA_MQTT_MASK);
|
||||
p->hop_start = (e->header.flags & PACKET_FLAGS_HOP_START_MASK) >> PACKET_FLAGS_HOP_START_SHIFT;
|
||||
p->which_payload_variant = e->encrypted ? meshtastic_MeshPacket_encrypted_tag : meshtastic_MeshPacket_decoded_tag;
|
||||
|
||||
unsigned char *payload = ((unsigned char *)e) + sizeof(PacketCacheEntry);
|
||||
PacketCacheMetadata m{};
|
||||
if (e->has_metadata) {
|
||||
memcpy(&m, (payload + e->payload_len), sizeof(m));
|
||||
p->rx_rssi = ((int)m.rx_rssi) - 200;
|
||||
p->rx_snr = ((float)m.rx_snr * 0.25f) - 30.0f;
|
||||
p->rx_time = m.rx_time;
|
||||
p->transport_mechanism = (meshtastic_MeshPacket_TransportMechanism)m.transport_mechanism;
|
||||
p->priority = (meshtastic_MeshPacket_Priority)m.priority;
|
||||
}
|
||||
if (e->encrypted) {
|
||||
memcpy(p->encrypted.bytes, payload, e->payload_len);
|
||||
p->encrypted.size = e->payload_len;
|
||||
} else {
|
||||
memcpy(p->decoded.payload.bytes, payload, e->payload_len);
|
||||
p->decoded.payload.size = e->payload_len;
|
||||
if (e->has_metadata) {
|
||||
// Decrypted-only metadata
|
||||
p->decoded.portnum = (meshtastic_PortNum)m.portnum;
|
||||
p->decoded.want_response = m.want_response;
|
||||
p->decoded.emoji = m.emoji;
|
||||
p->decoded.bitfield = m.bitfield;
|
||||
if (m.reply_id)
|
||||
p->decoded.reply_id = m.reply_id;
|
||||
else if (m.request_id)
|
||||
p->decoded.request_id = m.request_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Release a cache entry
|
||||
*/
|
||||
void PacketCache::release(PacketCacheEntry *e)
|
||||
{
|
||||
if (!e)
|
||||
return;
|
||||
remove(e);
|
||||
size -= sizeof(PacketCacheEntry) + e->payload_len + (e->has_metadata ? sizeof(PacketCacheMetadata) : 0);
|
||||
free(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a new entry into the hash table
|
||||
*/
|
||||
void PacketCache::insert(PacketCacheEntry *e)
|
||||
{
|
||||
assert(e);
|
||||
PacketHash h = PACKET_HASH(e->header.from, e->header.id);
|
||||
PacketCacheEntry **target = &buckets[PACKET_CACHE_BUCKET(h)];
|
||||
e->next = *target;
|
||||
*target = e;
|
||||
num_entries++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an entry from the hash table
|
||||
*/
|
||||
void PacketCache::remove(PacketCacheEntry *e)
|
||||
{
|
||||
assert(e);
|
||||
PacketHash h = PACKET_HASH(e->header.from, e->header.id);
|
||||
PacketCacheEntry **target = &buckets[PACKET_CACHE_BUCKET(h)];
|
||||
while (*target) {
|
||||
if (*target == e) {
|
||||
*target = e->next;
|
||||
e->next = NULL;
|
||||
num_entries--;
|
||||
break;
|
||||
} else {
|
||||
target = &(*target)->next;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
#pragma once
|
||||
#include "RadioInterface.h"
|
||||
|
||||
#define PACKET_HASH(a, b) ((((a ^ b) >> 16) ^ (a ^ b)) & 0xFFFF) // 16 bit fold of packet (from, id) tuple
|
||||
typedef uint16_t PacketHash;
|
||||
|
||||
#define PACKET_CACHE_BUCKETS 64 // Number of hash table buckets
|
||||
#define PACKET_CACHE_BUCKET(h) (((h >> 12) ^ (h >> 6) ^ h) & 0x3F) // Fold hash down to 6-bit bucket index
|
||||
|
||||
typedef struct PacketCacheEntry {
|
||||
PacketCacheEntry *next;
|
||||
PacketHeader header;
|
||||
uint16_t payload_len = 0;
|
||||
union {
|
||||
uint16_t bitfield;
|
||||
struct {
|
||||
uint8_t encrypted : 1; // Payload is encrypted
|
||||
uint8_t has_metadata : 1; // Payload includes PacketCacheMetadata
|
||||
uint8_t : 6; // Reserved for future use
|
||||
uint8_t : 8; // Reserved for future use
|
||||
};
|
||||
};
|
||||
} PacketCacheEntry;
|
||||
|
||||
typedef struct PacketCacheMetadata {
|
||||
PacketCacheMetadata() : _bitfield(0), reply_id(0), _bitfield2(0) {}
|
||||
union {
|
||||
uint32_t _bitfield;
|
||||
struct {
|
||||
uint16_t portnum : 9; // meshtastic_MeshPacket::decoded::portnum
|
||||
uint16_t want_response : 1; // meshtastic_MeshPacket::decoded::want_response
|
||||
uint16_t emoji : 1; // meshtastic_MeshPacket::decoded::emoji
|
||||
uint16_t bitfield : 5; // meshtastic_MeshPacket::decoded::bitfield (truncated)
|
||||
uint8_t rx_rssi : 8; // meshtastic_MeshPacket::rx_rssi (map via actual RSSI + 200)
|
||||
uint8_t rx_snr : 8; // meshtastic_MeshPacket::rx_snr (map via (p->rx_snr + 30.0f) / 0.25f)
|
||||
};
|
||||
};
|
||||
union {
|
||||
uint32_t reply_id; // meshtastic_MeshPacket::decoded.reply_id
|
||||
uint32_t request_id; // meshtastic_MeshPacket::decoded.request_id
|
||||
};
|
||||
uint32_t rx_time = 0; // meshtastic_MeshPacket::rx_time
|
||||
uint8_t transport_mechanism = 0; // meshtastic_MeshPacket::transport_mechanism
|
||||
struct {
|
||||
uint8_t _bitfield2;
|
||||
union {
|
||||
uint8_t priority : 7; // meshtastic_MeshPacket::priority
|
||||
uint8_t reserved : 1; // Reserved for future use
|
||||
};
|
||||
};
|
||||
} PacketCacheMetadata;
|
||||
|
||||
class PacketCache
|
||||
{
|
||||
public:
|
||||
PacketCacheEntry *cache(const meshtastic_MeshPacket *p, bool preserveMetadata);
|
||||
static void dump(void *dest, const PacketCacheEntry **entries, size_t num_entries);
|
||||
size_t dumpSize(const PacketCacheEntry **entries, size_t num_entries);
|
||||
PacketCacheEntry *find(NodeNum from, PacketId id);
|
||||
PacketCacheEntry *find(PacketHash h);
|
||||
bool load(void *src, PacketCacheEntry **entries, size_t num_entries);
|
||||
size_t getNumEntries() { return num_entries; }
|
||||
size_t getSize() { return size; }
|
||||
void rehydrate(const PacketCacheEntry *e, meshtastic_MeshPacket *p);
|
||||
void release(PacketCacheEntry *e);
|
||||
|
||||
private:
|
||||
PacketCacheEntry *buckets[PACKET_CACHE_BUCKETS]{};
|
||||
size_t num_entries = 0;
|
||||
size_t size = 0;
|
||||
void insert(PacketCacheEntry *e);
|
||||
void remove(PacketCacheEntry *e);
|
||||
};
|
||||
|
||||
extern PacketCache packetCache;
|
||||
+81
-31
@@ -38,7 +38,7 @@
|
||||
#include "mqtt/MQTT.h"
|
||||
#endif
|
||||
#include "Throttle.h"
|
||||
#include <RTC.h>
|
||||
#include "gps/RTC.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -1201,19 +1201,49 @@ void PhoneAPI::prefetchNodeInfos()
|
||||
onNowHasData(0);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
/// Derive a stable id for a replayed satellite-DB record. Unchanged history replayed on
|
||||
/// every reconnect must not look like a new packet to the phone's history/dedup - the id
|
||||
/// only changes when the underlying data (its timestamp) actually changes.
|
||||
// `kind` only needs to distinguish the record types that can otherwise collide (e.g. device
|
||||
// vs. environment metrics both replay as TELEMETRY_APP with the same node/last_heard) - pass
|
||||
// the payload's own variant/port constant.
|
||||
uint32_t makeReplayPacketId(NodeNum num, uint32_t timestamp, uint32_t kind)
|
||||
{
|
||||
uint32_t h = num;
|
||||
h = h * 2654435761u + timestamp;
|
||||
h = h * 2654435761u + kind;
|
||||
return h ? h : 1; // some clients treat id 0 as "unset"
|
||||
}
|
||||
|
||||
/// Populate hop_start/hop_limit from the node's real last-known hop count (if any) so a
|
||||
/// replayed packet doesn't read as "heard directly" when it wasn't.
|
||||
void setReplayHopFields(meshtastic_MeshPacket &pkt, const meshtastic_NodeInfoLite *header)
|
||||
{
|
||||
uint8_t hopLimit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
||||
uint8_t hopsAway = (header && header->has_hops_away) ? header->hops_away : 0;
|
||||
pkt.hop_start = hopLimit;
|
||||
pkt.hop_limit = hopsAway < hopLimit ? (uint8_t)(hopLimit - hopsAway) : 0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
meshtastic_MeshPacket PhoneAPI::makeReplayPositionPacket(NodeNum num, const meshtastic_PositionLite &pos)
|
||||
{
|
||||
// Shape this exactly like a fresh live broadcast Position from the peer so the
|
||||
// phone runs it through its normal "live position broadcast" handler path.
|
||||
// to=ourNum would read as a DM-from-peer and never lands in node detail UI.
|
||||
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default;
|
||||
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
||||
pkt.from = num;
|
||||
pkt.to = NODENUM_BROADCAST;
|
||||
pkt.id = generatePacketId();
|
||||
pkt.rx_time = pos.time;
|
||||
// Stable per-node/per-fix id: replaying the same unchanged history on every
|
||||
// reconnect must not look like a brand new packet to the phone's history/dedup.
|
||||
pkt.id = makeReplayPacketId(num, pkt.rx_time, meshtastic_PortNum_POSITION_APP);
|
||||
pkt.channel = 0;
|
||||
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
||||
pkt.hop_start = pkt.hop_limit;
|
||||
pkt.rx_snr = header ? header->snr : 0;
|
||||
setReplayHopFields(pkt, header);
|
||||
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
// Mark as if heard over the air, not internally generated
|
||||
pkt.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
|
||||
@@ -1231,13 +1261,13 @@ meshtastic_MeshPacket PhoneAPI::makeReplayTelemetryPacket(NodeNum num, const mes
|
||||
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default;
|
||||
pkt.from = num;
|
||||
pkt.to = NODENUM_BROADCAST;
|
||||
pkt.id = generatePacketId();
|
||||
// No native timestamp on telemetry packets here; use last_heard.
|
||||
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
||||
pkt.rx_time = header ? header->last_heard : 0;
|
||||
pkt.id = makeReplayPacketId(num, pkt.rx_time, meshtastic_Telemetry_device_metrics_tag);
|
||||
pkt.channel = 0;
|
||||
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
||||
pkt.hop_start = pkt.hop_limit;
|
||||
pkt.rx_snr = header ? header->snr : 0;
|
||||
setReplayHopFields(pkt, header);
|
||||
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
// Mark as if heard over the air, not internally generated - iOS client filters
|
||||
// TRANSPORT_INTERNAL packets out of broadcast peer state updates.
|
||||
@@ -1337,12 +1367,12 @@ meshtastic_MeshPacket PhoneAPI::makeReplayEnvironmentPacket(uint32_t num, const
|
||||
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default;
|
||||
pkt.from = num;
|
||||
pkt.to = NODENUM_BROADCAST;
|
||||
pkt.id = generatePacketId();
|
||||
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
||||
pkt.rx_time = header ? header->last_heard : 0;
|
||||
pkt.id = makeReplayPacketId(num, pkt.rx_time, meshtastic_Telemetry_environment_metrics_tag);
|
||||
pkt.channel = 0;
|
||||
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
||||
pkt.hop_start = pkt.hop_limit;
|
||||
pkt.rx_snr = header ? header->snr : 0;
|
||||
setReplayHopFields(pkt, header);
|
||||
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
// Mark as if heard over the air, not internally generated - iOS client filters
|
||||
// TRANSPORT_INTERNAL packets out of broadcast peer state updates.
|
||||
@@ -1400,13 +1430,13 @@ meshtastic_MeshPacket PhoneAPI::makeReplayStatusPacket(uint32_t num, const mesht
|
||||
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default;
|
||||
pkt.from = num;
|
||||
pkt.to = NODENUM_BROADCAST;
|
||||
pkt.id = generatePacketId();
|
||||
// StatusMessage has no native timestamp; use last_heard.
|
||||
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
||||
pkt.rx_time = header ? header->last_heard : 0;
|
||||
pkt.id = makeReplayPacketId(num, pkt.rx_time, meshtastic_PortNum_NODE_STATUS_APP);
|
||||
pkt.channel = 0;
|
||||
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
||||
pkt.hop_start = pkt.hop_limit;
|
||||
pkt.rx_snr = header ? header->snr : 0;
|
||||
setReplayHopFields(pkt, header);
|
||||
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
// Mark as if heard over the air, not internally generated - client filters
|
||||
pkt.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
|
||||
@@ -1667,6 +1697,19 @@ bool PhoneAPI::wasSeenRecently(uint32_t id)
|
||||
return false;
|
||||
}
|
||||
|
||||
PhoneAPI::LocalAdminGate PhoneAPI::classifyLocalAdminPacket(const meshtastic_MeshPacket &p, bool adminAuthorized,
|
||||
meshtastic_AdminMessage &outAdmin)
|
||||
{
|
||||
if (p.which_payload_variant != meshtastic_MeshPacket_decoded_tag || p.decoded.portnum != meshtastic_PortNum_ADMIN_APP)
|
||||
return LocalAdminGate::NotAdmin;
|
||||
outAdmin = meshtastic_AdminMessage_init_zero;
|
||||
if (!pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_AdminMessage_msg, &outAdmin))
|
||||
return LocalAdminGate::NotAdmin; // undecodable: let the normal reject path respond
|
||||
if (outAdmin.which_payload_variant == meshtastic_AdminMessage_lockdown_auth_tag)
|
||||
return LocalAdminGate::LockdownAuth; // the passphrase itself; authenticate regardless of prior state
|
||||
return adminAuthorized ? LocalAdminGate::AuthorizedPassThrough : LocalAdminGate::DropUnauthorized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a packet that the phone wants us to send. It is our responsibility to free the packet to the pool
|
||||
*/
|
||||
@@ -1691,26 +1734,33 @@ bool PhoneAPI::handleToRadioPacket(meshtastic_MeshPacket &p)
|
||||
// the gate here closes that race and covers H6/H7 from the
|
||||
// audit: get_config_request and set_config from unauthed
|
||||
// clients no longer reach AdminModule at all.
|
||||
if (p.from == 0 && p.which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
|
||||
p.decoded.portnum == meshtastic_PortNum_ADMIN_APP) {
|
||||
// Gate on the connection, not the wire `from`, which a client can forge to a non-zero value to
|
||||
// bypass the check before MeshService normalizes it back to a local identity.
|
||||
// Scope the decoded admin message: it holds the plaintext passphrase, so bound its lifetime.
|
||||
{
|
||||
meshtastic_AdminMessage admin = meshtastic_AdminMessage_init_zero;
|
||||
if (pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_AdminMessage_msg, &admin)) {
|
||||
if (admin.which_payload_variant == meshtastic_AdminMessage_lockdown_auth_tag) {
|
||||
handleLockdownAuthInline(admin.lockdown_auth);
|
||||
// Wipe the decoded passphrase scratch - the byte array in
|
||||
// p.decoded.payload.bytes is wiped by handleLockdownAuthInline.
|
||||
volatile uint8_t *adminVol = const_cast<volatile uint8_t *>(admin.lockdown_auth.passphrase.bytes);
|
||||
for (size_t i = 0; i < sizeof(admin.lockdown_auth.passphrase.bytes); i++)
|
||||
adminVol[i] = 0;
|
||||
return true;
|
||||
}
|
||||
if (!getAdminAuthorized()) {
|
||||
LOG_WARN("Lockdown: dropping admin payload variant=%d from unauthorized connection", admin.which_payload_variant);
|
||||
return false;
|
||||
}
|
||||
switch (classifyLocalAdminPacket(p, getAdminAuthorized(), admin)) {
|
||||
case LocalAdminGate::LockdownAuth: {
|
||||
handleLockdownAuthInline(admin.lockdown_auth);
|
||||
// The encoded wipe is the security-critical one: nothing else clears the passphrase from
|
||||
// the packet buffer. handleLockdownAuthInline already zeroes the decoded copy up to its
|
||||
// size; re-zero the full scratch capacity here as defense in depth.
|
||||
volatile uint8_t *adminVol = const_cast<volatile uint8_t *>(admin.lockdown_auth.passphrase.bytes);
|
||||
for (size_t i = 0; i < sizeof(admin.lockdown_auth.passphrase.bytes); i++)
|
||||
adminVol[i] = 0;
|
||||
volatile uint8_t *encodedVol = const_cast<volatile uint8_t *>(p.decoded.payload.bytes);
|
||||
for (size_t i = 0; i < sizeof(p.decoded.payload.bytes); i++)
|
||||
encodedVol[i] = 0;
|
||||
p.decoded.payload.size = 0; // keep the length consistent with the wiped buffer
|
||||
return true;
|
||||
}
|
||||
case LocalAdminGate::DropUnauthorized:
|
||||
LOG_WARN("Lockdown: dropping admin payload variant=%d from unauthorized connection", admin.which_payload_variant);
|
||||
return false;
|
||||
case LocalAdminGate::NotAdmin:
|
||||
case LocalAdminGate::AuthorizedPassThrough:
|
||||
break; // normal handling
|
||||
}
|
||||
// pb_decode failure: fall through to normal handling so the
|
||||
// regular Router/AdminModule reject path can respond.
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -319,4 +319,18 @@ class PhoneAPI
|
||||
|
||||
/// If the mesh service tells us fromNum has changed, tell the phone
|
||||
virtual int onNotify(uint32_t newValue) override;
|
||||
|
||||
public:
|
||||
/// How the lockdown admin gate should treat a phone->radio packet.
|
||||
enum class LocalAdminGate {
|
||||
NotAdmin, ///< Not a decodable ADMIN_APP payload; normal handling.
|
||||
LockdownAuth, ///< A lockdown_auth payload; authenticate the connection inline.
|
||||
DropUnauthorized, ///< Admin payload from a connection that has not authenticated; drop.
|
||||
AuthorizedPassThrough, ///< Admin payload from an authorized connection; normal handling.
|
||||
};
|
||||
|
||||
/// Classify a phone->radio packet for the lockdown admin gate, ignoring the wire `from` (which a
|
||||
/// client can forge) and deciding on the connection's authorization. Fills outAdmin for lockdown.
|
||||
static LocalAdminGate classifyLocalAdminPacket(const meshtastic_MeshPacket &p, bool adminAuthorized,
|
||||
meshtastic_AdminMessage &outAdmin);
|
||||
};
|
||||
|
||||
@@ -184,11 +184,9 @@ const RegionInfo regions[] = {
|
||||
|
||||
/*
|
||||
433,05-434,7 Mhz 10 mW
|
||||
868,0-868,6 Mhz 25 mW
|
||||
https://nkrzi.gov.ua/images/upload/256/5810/PDF_UUZ_19_01_2016.pdf
|
||||
https://zakon.rada.gov.ua/laws/show/262-2026-п
|
||||
*/
|
||||
RDEF(UA_433, 433.0f, 434.7f, 10, 10, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
RDEF(UA_868, 868.0f, 868.6f, 1, 14, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
|
||||
/*
|
||||
Malaysia
|
||||
|
||||
+73
-36
@@ -5,7 +5,7 @@
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PositionPrecision.h"
|
||||
#include "RTC.h"
|
||||
#include "gps/RTC.h"
|
||||
|
||||
#include "configuration.h"
|
||||
#include "main.h"
|
||||
@@ -452,7 +452,7 @@ void Router::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Rout
|
||||
}
|
||||
|
||||
#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.
|
||||
p->xeddsa_signed = false;
|
||||
@@ -483,17 +483,17 @@ bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p, size_t encodedDataSize)
|
||||
return false;
|
||||
} else {
|
||||
// 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
|
||||
// the size of the encoded Data exactly as the sender built it (or 0 to size p->decoded
|
||||
// canonically); with no signature field present it is the unsigned base, and adding
|
||||
// XEDDSA_SIGNATURE_FIELD_BYTES mirrors the sender-side signedDataFits() gate per packet,
|
||||
// whatever fields the Data carried. Unicast/PKI packets and broadcasts too big to carry a
|
||||
// signature are never signed, so they must not be hard-failed here even for a known signer.
|
||||
// non-PKI broadcast whose signed encoding would still fit the LoRa frame. Size p->decoded
|
||||
// canonically so this counts the same fields the sender's signedDataFits() gate counted;
|
||||
// adding XEDDSA_SIGNATURE_FIELD_BYTES to that unsigned base mirrors it exactly, whatever
|
||||
// fields the Data carried. Unicast/PKI packets and broadcasts too big to carry a signature
|
||||
// are never signed, so they must not be hard-failed here even for a known signer.
|
||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(p->from);
|
||||
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
|
||||
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);
|
||||
return false;
|
||||
}
|
||||
@@ -524,36 +524,60 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
bool decrypted = false;
|
||||
ChannelIndex chIndex = 0;
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||
// Attempt PKI decryption first. The sender's key may come from the hot
|
||||
// store or the warm tier (nodes evicted from the hot store keep their key
|
||||
// there), so DMs from long-tail nodes still decrypt.
|
||||
meshtastic_NodeInfoLite_public_key_t fromKey = {0, {0}};
|
||||
if (p->channel == 0 && isToUs(p) && p->to > 0 && !isBroadcast(p->to) && nodeDB->copyPublicKey(p->from, fromKey) &&
|
||||
nodeDB->getMeshNode(p->to) != nullptr && nodeDB->getMeshNode(p->to)->public_key.size > 0 &&
|
||||
rawSize > MESHTASTIC_PKC_OVERHEAD) {
|
||||
LOG_DEBUG("Attempt PKI decryption");
|
||||
// Resolve the sender's public key: prefer the one stored in NodeDB (hot store or warm tier), else
|
||||
// fall back to a not-yet-committed key held during an in-progress key-verification handshake.
|
||||
meshtastic_NodeInfoLite_public_key_t remotePublic = {0, {0}};
|
||||
bool haveRemoteKey = nodeDB->copyPublicKey(p->from, remotePublic) || crypto->getPendingPublicKey(p->from, remotePublic);
|
||||
|
||||
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!");
|
||||
|
||||
meshtastic_Data decodedtmp;
|
||||
memset(&decodedtmp, 0, sizeof(decodedtmp));
|
||||
rawSize -= MESHTASTIC_PKC_OVERHEAD;
|
||||
if (pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp) &&
|
||||
size_t payloadSize = rawSize - MESHTASTIC_PKC_OVERHEAD;
|
||||
if (pb_decode_from_bytes(bytes, payloadSize, &meshtastic_Data_msg, &decodedtmp) &&
|
||||
decodedtmp.portnum != meshtastic_PortNum_UNKNOWN_APP) {
|
||||
decrypted = true;
|
||||
rawSize = payloadSize; // commit the overhead subtraction only on full success
|
||||
LOG_INFO("Packet decrypted using PKI!");
|
||||
p->pki_encrypted = true;
|
||||
memcpy(p->public_key.bytes, fromKey.bytes, 32);
|
||||
memcpy(p->public_key.bytes, remotePublic.bytes, 32);
|
||||
p->public_key.size = 32;
|
||||
p->decoded = decodedtmp;
|
||||
p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded
|
||||
if (viaAdminKey) {
|
||||
// Persist the admin key for the sender so future packets take the fast path and we can
|
||||
// PKI-reply; p->from is bound into the AEAD nonce, so the trusted admin authenticated it.
|
||||
meshtastic_NodeInfoLite *fromNode = nodeDB->getOrCreateMeshNode(p->from);
|
||||
if (fromNode != nullptr)
|
||||
fromNode->public_key = remotePublic;
|
||||
}
|
||||
} else {
|
||||
// AEAD already authenticated this ciphertext, so no other candidate could decode it -
|
||||
// the payload is simply malformed.
|
||||
LOG_ERROR("PKC Decrypted, but pb_decode failed!");
|
||||
return DecodeState::DECODE_FAILURE;
|
||||
}
|
||||
} else {
|
||||
LOG_WARN("PKC decrypt attempted but failed!");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -597,17 +621,17 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
if (decrypted) {
|
||||
// parsing was successful
|
||||
p->channel = chIndex; // change to store the index instead of the hash
|
||||
if (p->decoded.has_bitfield)
|
||||
p->decoded.want_response |= p->decoded.bitfield & BITFIELD_WANT_RESPONSE_MASK;
|
||||
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA)
|
||||
// rawSize is the size of the encoded Data exactly as the sender built it (the PKI branch's
|
||||
// MESHTASTIC_PKC_OVERHEAD subtraction preserves that, and PKI packets are unicast so the
|
||||
// downgrade predicate ignores them anyway).
|
||||
if (!checkXeddsaReceivePolicy(p, rawSize))
|
||||
// Runs before the bitfield merge below: that merge can set want_response, adding wire bytes
|
||||
// the sender never encoded and skewing the policy's sizing of p->decoded.
|
||||
if (!checkXeddsaReceivePolicy(p))
|
||||
return DecodeState::DECODE_FAILURE;
|
||||
#endif
|
||||
|
||||
if (p->decoded.has_bitfield)
|
||||
p->decoded.want_response |= p->decoded.bitfield & BITFIELD_WANT_RESPONSE_MASK;
|
||||
|
||||
/* Not actually ever used.
|
||||
// Decompress if needed. jm
|
||||
if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP) {
|
||||
@@ -729,7 +753,7 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
|
||||
|
||||
LOG_DEBUG("Original length - %d ", p->decoded.payload.size);
|
||||
LOG_DEBUG("Compressed length - %d ", compressed_len);
|
||||
LOG_DEBUG("Original message - %s ", p->decoded.payload.bytes);
|
||||
LOG_DEBUG("Original message - %.*s ", (int)p->decoded.payload.size, p->decoded.payload.bytes);
|
||||
|
||||
// If the compressed length is greater than or equal to the original size, don't use the compressed form
|
||||
if (compressed_len >= p->decoded.payload.size) {
|
||||
@@ -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
|
||||
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||
// Destination key from the hot store or the warm tier (evicted
|
||||
// long-tail nodes keep their key there)
|
||||
// Resolve the destination's public key: prefer NodeDB (hot store or warm tier - evicted
|
||||
// long-tail nodes keep their key there), otherwise (for a key-verification follow-on packet
|
||||
// that explicitly requested PKI) fall back to the not-yet-verified key held during an
|
||||
// in-progress handshake. This lets us DH-encode the follow-on packet before the peer's key
|
||||
// has been committed to NodeDB.
|
||||
meshtastic_NodeInfoLite_public_key_t destKey = {0, {0}};
|
||||
bool haveDestKey = nodeDB->copyPublicKey(p->to, destKey);
|
||||
if (!haveDestKey && p->pki_encrypted && p->decoded.portnum == meshtastic_PortNum_KEY_VERIFICATION_APP &&
|
||||
crypto->getPendingPublicKey(p->to, destKey)) {
|
||||
haveDestKey = true;
|
||||
}
|
||||
// We may want to retool things so we can send a PKC packet when the client specifies a key and nodenum, even if the node
|
||||
// is not in the local nodedb
|
||||
// First, only PKC encrypt packets we are originating
|
||||
@@ -779,11 +810,17 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
|
||||
config.security.private_key.size == 32 && !isBroadcast(p->to) &&
|
||||
// Some portnums either make no sense to send with PKC
|
||||
p->decoded.portnum != meshtastic_PortNum_TRACEROUTE_APP && p->decoded.portnum != meshtastic_PortNum_NODEINFO_APP &&
|
||||
p->decoded.portnum != meshtastic_PortNum_ROUTING_APP && p->decoded.portnum != meshtastic_PortNum_POSITION_APP) {
|
||||
p->decoded.portnum != meshtastic_PortNum_ROUTING_APP && p->decoded.portnum != meshtastic_PortNum_POSITION_APP &&
|
||||
// We allow Key Verification messages to be sent without a known destination key, since the point of those messages is
|
||||
// to exchange keys. The first exchange (no usable key yet) falls through to channel encryption; the follow-on packet
|
||||
// uses the pending key resolved into haveDestKey/destKey above.
|
||||
// Though possible the first packet each direction should go non-pkc
|
||||
// to handle the case where the remote node has our key, but we don't have theirs.
|
||||
!(p->decoded.portnum == meshtastic_PortNum_KEY_VERIFICATION_APP && !haveDestKey)) {
|
||||
LOG_DEBUG("Use PKI!");
|
||||
if (numbytes + MESHTASTIC_HEADER_LENGTH + MESHTASTIC_PKC_OVERHEAD > MAX_LORA_PAYLOAD_LEN)
|
||||
return meshtastic_Routing_Error_TOO_LARGE;
|
||||
// Check for a known public key for the destination
|
||||
// Check for a usable public key for the destination (NodeDB or a pending key-verification key)
|
||||
if (!haveDestKey) {
|
||||
LOG_WARN("Unknown public key for destination node 0x%08x (portnum %d), refusing to send legacy DM", p->to,
|
||||
p->decoded.portnum);
|
||||
|
||||
+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
|
||||
* 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
|
||||
* p->decoded canonically instead (for already-decoded ingress such as plaintext-MQTT downlink,
|
||||
* which bypasses perhapsDecode's crypto path).
|
||||
* The fit test sizes p->decoded with the real encoder, so it measures the fields the sender
|
||||
* encoded rather than any raw wire length.
|
||||
*
|
||||
* The caller MUST hold cryptLock: verification runs through the shared CryptoEngine key cache.
|
||||
* (perhapsDecode already holds it; other call sites must take it themselves.)
|
||||
*
|
||||
* @return false if the packet must be dropped.
|
||||
*/
|
||||
bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p, size_t encodedDataSize = 0);
|
||||
bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p);
|
||||
#endif
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
#if 0
|
||||
// Read/write a register we are not using (only used for FSK mode) to test SPI comms
|
||||
uint8_t crcLSB = 0;
|
||||
int err = lora.readRegister(SX126X_REG_CRC_POLYNOMIAL_LSB, &crcLSB, 1);
|
||||
if(err != RADIOLIB_ERR_NONE)
|
||||
RECORD_CRITICALERROR(CriticalErrorCode_SX1262Failure);
|
||||
|
||||
//if(crcLSB != 0x0f)
|
||||
// RECORD_CRITICALERROR(CriticalErrorCode_SX1262Failure);
|
||||
|
||||
crcLSB = 0x5a;
|
||||
err = lora.writeRegister(SX126X_REG_CRC_POLYNOMIAL_LSB, &crcLSB, 1);
|
||||
if(err != RADIOLIB_ERR_NONE)
|
||||
RECORD_CRITICALERROR(CriticalErrorCode_SX1262Failure);
|
||||
|
||||
err = lora.readRegister(SX126X_REG_CRC_POLYNOMIAL_LSB, &crcLSB, 1);
|
||||
if(err != RADIOLIB_ERR_NONE)
|
||||
RECORD_CRITICALERROR(CriticalErrorCode_SX1262Failure);
|
||||
|
||||
if(crcLSB != 0x5a)
|
||||
RECORD_CRITICALERROR(CriticalErrorCode_SX1262Failure);
|
||||
// If we got this far register accesses (and therefore SPI comms) are good
|
||||
#endif
|
||||
|
||||
if (res == RADIOLIB_ERR_NONE)
|
||||
res = lora.setCRC(RADIOLIB_SX126X_LORA_CRC_ON);
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#include "StreamAPI.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "Throttle.h"
|
||||
#include "concurrency/LockGuard.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/RTC.h"
|
||||
|
||||
#define START1 0x94
|
||||
#define START2 0xc3
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "TransmitHistory.h"
|
||||
#include "FSCommon.h"
|
||||
#include "RTC.h"
|
||||
#include "SPILock.h"
|
||||
#include "gps/RTC.h"
|
||||
#include <Throttle.h>
|
||||
|
||||
#ifdef FSCom
|
||||
|
||||
+57
-39
@@ -13,12 +13,11 @@
|
||||
|
||||
#if defined(NRF52840_XXAA)
|
||||
#include "flash/flash_nrf5x.h"
|
||||
#define WARM_RING_MAGIC 0x324E5257u // "WRN2" - v2: last_heard low bits carry role + protected category
|
||||
#define WARM_RING_MAGIC 0x334E5257u // "WRN3" - v3: last_heard low bits carry role + protected + signer
|
||||
#define WARM_RING_MAGIC_V2 0x324E5257u // "WRN2" - v2: role + protected only; bit 6 was still timestamp.
|
||||
#define WARM_RING_MAGIC_V1 0x474E5257u // "WRNG" - v1: last_heard was a plain timestamp.
|
||||
// v1 pages are still read on upgrade: we keep each record's identity + public key but
|
||||
// DISCARD its last_heard (the old timestamp would be misread as role/protected bits).
|
||||
// Records re-rank and re-learn their role on the next contact. Legacy pages convert to
|
||||
// v2 naturally as the ring rotates.
|
||||
// Older pages are still read on upgrade: v1 keeps identity + key but discards last_heard,
|
||||
// v2 keeps the word but clears bit 6, which was still part of the timestamp then.
|
||||
// A tombstone is an entry record whose last_heard is all-ones - getTime()
|
||||
// (unix seconds) cannot reach 0xFFFFFFFF until 2106, and erased flash is
|
||||
// detected via num == 0xFFFFFFFF before last_heard is ever inspected.
|
||||
@@ -34,10 +33,13 @@ struct WarmStoreHeader {
|
||||
};
|
||||
static_assert(sizeof(WarmStoreHeader) == 16, "header layout is part of the persistence format");
|
||||
|
||||
#define WARM_STORE_MAGIC 0x324D5257u // "WRM2" - v2: last_heard low bits carry role + protected category
|
||||
#define WARM_STORE_MAGIC 0x334D5257u // "WRM3" - v3: last_heard low bits carry role + protected + signer
|
||||
#define WARM_STORE_MAGIC_V2 \
|
||||
0x324D5257u // "WRM2" - v2: role + protected only; bit 6 was still timestamp. On upgrade
|
||||
// we clear the signer bit, then rewrite as v3.
|
||||
#define WARM_STORE_MAGIC_V1 \
|
||||
0x314D5257u // "WRM1" - v1: last_heard was a plain timestamp. On upgrade we keep
|
||||
// identity + key but discard last_heard, then rewrite as v2.
|
||||
// identity + key but discard last_heard, then rewrite as v3.
|
||||
|
||||
#ifdef FSCom
|
||||
static const char *warmFileName = "/prefs/warm.dat";
|
||||
@@ -134,18 +136,18 @@ WarmNodeEntry *WarmNodeStore::place(NodeNum num, uint32_t lastHeard, const uint8
|
||||
return slot;
|
||||
}
|
||||
|
||||
bool WarmNodeStore::absorb(NodeNum num, uint32_t lastHeard, const uint8_t *key32, uint8_t role, uint8_t protectedCat)
|
||||
bool WarmNodeStore::absorb(NodeNum num, uint32_t lastHeard, const uint8_t *key32, uint8_t role, uint8_t protectedCat, bool signer)
|
||||
{
|
||||
// Pack role + protected category into the low bits of last_heard. place() and ring
|
||||
// replay store the raw word verbatim, so the metadata round-trips through flash.
|
||||
const uint32_t packed = warmPackLastHeard(lastHeard, role, protectedCat);
|
||||
// Pack role + protected category + signer into the low bits of last_heard. place() and
|
||||
// ring replay store the raw word verbatim, so the metadata round-trips through flash.
|
||||
const uint32_t packed = warmPackLastHeard(lastHeard, role, protectedCat, signer);
|
||||
const WarmNodeEntry *slot = place(num, packed, key32);
|
||||
if (!slot)
|
||||
return false;
|
||||
persistEntry(*slot);
|
||||
LOG_MIGRATION("WarmStore absorb 0x%08x key=%d last_heard=%u role=%u prot=%u (now %u/%u)", (unsigned)num,
|
||||
LOG_MIGRATION("WarmStore absorb 0x%08x key=%d last_heard=%u role=%u prot=%u signer=%u (now %u/%u)", (unsigned)num,
|
||||
keyIsSet(slot->public_key) ? 1 : 0, (unsigned)warmTimeOf(*slot), (unsigned)role, (unsigned)protectedCat,
|
||||
(unsigned)count(), (unsigned)capacity());
|
||||
signer ? 1u : 0u, (unsigned)count(), (unsigned)capacity());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -258,19 +260,24 @@ bool WarmNodeStore::saveIfDirty()
|
||||
// (stranded live entries re-appended, then erased). Flash access holds spiLock -
|
||||
// the page cache is shared with InternalFS/LittleFS.
|
||||
|
||||
bool WarmNodeStore::ringReadHeader(uint8_t page, WarmPageHeader &h, bool *legacy) const
|
||||
bool WarmNodeStore::ringReadHeader(uint8_t page, WarmPageHeader &h, WarmFormat *fmt) const
|
||||
{
|
||||
flash_nrf5x_read(&h, WARM_FLASH_PAGE_ADDR(page), sizeof(h));
|
||||
if (h.seq == 0xFFFFFFFFu)
|
||||
return false; // erased page
|
||||
if (h.magic == WARM_RING_MAGIC) {
|
||||
if (legacy)
|
||||
*legacy = false;
|
||||
if (fmt)
|
||||
*fmt = WarmFormat::Current;
|
||||
return true;
|
||||
}
|
||||
if (h.magic == WARM_RING_MAGIC_V2) {
|
||||
if (fmt)
|
||||
*fmt = WarmFormat::V2; // replay it, but clear the signer bit (see WARM_RING_MAGIC_V2)
|
||||
return true;
|
||||
}
|
||||
if (h.magic == WARM_RING_MAGIC_V1) {
|
||||
if (legacy)
|
||||
*legacy = true; // v1 page: replay it, but discard last_heard (see WARM_RING_MAGIC_V1)
|
||||
if (fmt)
|
||||
*fmt = WarmFormat::V1; // replay it, but discard last_heard (see WARM_RING_MAGIC_V1)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -386,13 +393,13 @@ void WarmNodeStore::load()
|
||||
// Order valid pages by ascending seq so replay applies oldest first
|
||||
uint8_t order[WARM_FLASH_PAGES] = {};
|
||||
uint32_t seqs[WARM_FLASH_PAGES] = {};
|
||||
bool legacyOf[WARM_FLASH_PAGES] = {}; // per-page: v1 (WRNG) → discard last_heard on replay
|
||||
WarmFormat fmtOf[WARM_FLASH_PAGES] = {}; // per-page on-flash format; older ones are normalised on replay
|
||||
uint8_t nValid = 0;
|
||||
uint8_t nCorrupt = 0;
|
||||
for (uint8_t p = 0; p < WARM_FLASH_PAGES; p++) {
|
||||
WarmPageHeader h;
|
||||
bool legacy = false;
|
||||
if (!ringReadHeader(p, h, &legacy)) {
|
||||
WarmFormat fmt = WarmFormat::Current;
|
||||
if (!ringReadHeader(p, h, &fmt)) {
|
||||
// An erased page reads back all-ones; any other magic is a
|
||||
// partially-written or bit-rotted header we're dropping, so flag it
|
||||
// rather than silently treating the loss as a clean empty ring.
|
||||
@@ -400,7 +407,7 @@ void WarmNodeStore::load()
|
||||
nCorrupt++;
|
||||
continue;
|
||||
}
|
||||
legacyOf[p] = legacy;
|
||||
fmtOf[p] = fmt;
|
||||
uint8_t pos = nValid;
|
||||
while (pos > 0 && static_cast<int32_t>(h.seq - seqs[pos - 1]) < 0) {
|
||||
order[pos] = order[pos - 1];
|
||||
@@ -427,7 +434,7 @@ void WarmNodeStore::load()
|
||||
uint32_t migrated = 0;
|
||||
for (uint8_t k = 0; k < nValid; k++) {
|
||||
const uint8_t p = order[k];
|
||||
const bool legacy = legacyOf[p];
|
||||
const WarmFormat fmt = fmtOf[p];
|
||||
uint16_t slot = 0;
|
||||
for (; slot < kRecordsPerPage; slot++) {
|
||||
WarmNodeEntry rec;
|
||||
@@ -444,12 +451,15 @@ void WarmNodeStore::load()
|
||||
memset(e, 0, sizeof(*e));
|
||||
}
|
||||
} else {
|
||||
// v1 (legacy) record: keep identity + key, but discard the old timestamp -
|
||||
// its low bits would otherwise be misread as role/protected metadata.
|
||||
// Normalise older records: v1's timestamp would be misread as role/protected,
|
||||
// and v2's bit 6 as a signer we never verified.
|
||||
uint32_t lh = rec.last_heard;
|
||||
if (legacy) {
|
||||
if (fmt == WarmFormat::V1) {
|
||||
lh = 0;
|
||||
migrated++;
|
||||
} else if (fmt == WarmFormat::V2) {
|
||||
lh &= ~(WARM_SIGNER_MASK << WARM_SIGNER_SHIFT);
|
||||
migrated++;
|
||||
}
|
||||
const WarmNodeEntry *e = place(rec.num, lh, rec.public_key);
|
||||
if (e)
|
||||
@@ -460,17 +470,16 @@ void WarmNodeStore::load()
|
||||
activePage = p;
|
||||
writeSlot = slot;
|
||||
nextSeq = seqs[k] + 1;
|
||||
// If the head is a v1 page, force the next append to rotate into a fresh v2 page,
|
||||
// so new (v2) records never land in a page whose header says v1 (which would make
|
||||
// a later load discard their last_heard - including the role/protected we just set).
|
||||
if (legacy)
|
||||
// Rotate rather than append into an older-format page: a later load would
|
||||
// normalise the new records to that page's format and drop metadata.
|
||||
if (fmt != WarmFormat::Current)
|
||||
writeSlot = kRecordsPerPage;
|
||||
}
|
||||
}
|
||||
if (nCorrupt)
|
||||
LOG_WARN("WarmStore: dropped %u corrupt ring page(s), some nodes lost", nCorrupt);
|
||||
if (migrated)
|
||||
LOG_INFO("WarmStore: migrated %u v1 record(s) (kept key, discarded last_heard)", (unsigned)migrated);
|
||||
LOG_INFO("WarmStore: migrated %u legacy record(s) to the current format", (unsigned)migrated);
|
||||
LOG_INFO("WarmStore: replayed %u ring records -> %u live nodes (page %u, slot %u)", (unsigned)replayed, (unsigned)count(),
|
||||
activePage, writeSlot);
|
||||
}
|
||||
@@ -537,10 +546,14 @@ void WarmNodeStore::load()
|
||||
LOG_WARN("WarmStore: %s header read failed, starting empty", warmFileName);
|
||||
return;
|
||||
}
|
||||
// v1 (WRM1) is still accepted: same record size, but its last_heard was a plain
|
||||
// timestamp. We keep identity + key and discard last_heard on load (see below).
|
||||
const bool legacy = (h.magic == WARM_STORE_MAGIC_V1);
|
||||
if ((h.magic != WARM_STORE_MAGIC && !legacy) || h.entrySize != sizeof(WarmNodeEntry) || h.count > WARM_NODE_COUNT) {
|
||||
// Older snapshots are still accepted: same record size, fewer metadata bits in
|
||||
// last_heard. Both are normalised to the current format below.
|
||||
const bool known = h.magic == WARM_STORE_MAGIC || h.magic == WARM_STORE_MAGIC_V2 || h.magic == WARM_STORE_MAGIC_V1;
|
||||
const WarmFormat fmt = h.magic == WARM_STORE_MAGIC_V1 ? WarmFormat::V1
|
||||
: h.magic == WARM_STORE_MAGIC_V2 ? WarmFormat::V2
|
||||
: WarmFormat::Current;
|
||||
const bool legacy = fmt != WarmFormat::Current;
|
||||
if (!known || h.entrySize != sizeof(WarmNodeEntry) || h.count > WARM_NODE_COUNT) {
|
||||
f.close();
|
||||
LOG_WARN("WarmStore: %s header invalid (magic=0x%08x entrySize=%u count=%u), starting empty", warmFileName, h.magic,
|
||||
h.entrySize, h.count);
|
||||
@@ -560,19 +573,24 @@ void WarmNodeStore::load()
|
||||
memset(entries, 0, WARM_NODE_COUNT * sizeof(WarmNodeEntry));
|
||||
return;
|
||||
}
|
||||
if (legacy) {
|
||||
// Migrate v1 → v2: discard the old last_heard (its low bits would be misread as
|
||||
// role/protected); keep num + public_key. Mark dirty so save() rewrites as v2.
|
||||
// Normalise older records, then mark dirty so save() rewrites in the current format:
|
||||
// v1's timestamp would be misread as role/protected, v2's bit 6 as an unverified signer.
|
||||
if (fmt == WarmFormat::V1) {
|
||||
for (size_t i = 0; i < WARM_NODE_COUNT; i++)
|
||||
if (entries[i].num)
|
||||
entries[i].last_heard = 0;
|
||||
dirty = true;
|
||||
} else if (fmt == WarmFormat::V2) {
|
||||
for (size_t i = 0; i < WARM_NODE_COUNT; i++)
|
||||
if (entries[i].num)
|
||||
entries[i].last_heard &= ~(WARM_SIGNER_MASK << WARM_SIGNER_SHIFT);
|
||||
dirty = true;
|
||||
}
|
||||
} else {
|
||||
f.close();
|
||||
}
|
||||
LOG_INFO("WarmStore: loaded %u warm nodes from %s%s", h.count, warmFileName,
|
||||
legacy ? " (v1 migrated: discarded last_heard)" : "");
|
||||
legacy ? " (migrated from an older format)" : "");
|
||||
}
|
||||
|
||||
bool WarmNodeStore::save()
|
||||
|
||||
+22
-10
@@ -43,27 +43,33 @@ static_assert(sizeof(WarmNodeEntry) == 40, "WarmNodeEntry must stay 40 B - persi
|
||||
//
|
||||
// The warm tier only uses last_heard to LRU-rank evicted (long-tail) nodes, so ~minute
|
||||
// recency resolution is plenty. We reclaim the low WARM_META_BITS of that field to carry
|
||||
// the evicted node's device role + a protected category, at zero cost to record size
|
||||
// (entry stays 40 B; no RAM/flash growth). The high bits remain a real unix-seconds
|
||||
// timestamp quantised to (1 << WARM_META_BITS) seconds.
|
||||
// the evicted node's device role, a protected category + a signer flag, at zero cost to
|
||||
// record size (entry stays 40 B; no RAM/flash growth). The high bits remain a real
|
||||
// unix-seconds timestamp quantised to (1 << WARM_META_BITS) seconds.
|
||||
//
|
||||
// Safe because: a real timestamp can never be all-ones (the tombstone sentinel) before
|
||||
// 2106, and tombstones/erased flash are detected via num before last_heard is read. Only
|
||||
// the LOW bits are stolen - the high (era) bits are untouched, so the time range is intact.
|
||||
static constexpr uint32_t WARM_META_BITS = 6; // role(4) + protected(2)
|
||||
static constexpr uint32_t WARM_META_MASK = (1u << WARM_META_BITS) - 1; // 0x3F → 64 s quantum
|
||||
static constexpr uint32_t WARM_TIME_MASK = ~WARM_META_MASK; // 0xFFFFFFC0
|
||||
static constexpr uint32_t WARM_META_BITS = 7; // role(4) + protected(2) + signer(1)
|
||||
static constexpr uint32_t WARM_META_MASK = (1u << WARM_META_BITS) - 1; // 0x7F → 128 s quantum
|
||||
static constexpr uint32_t WARM_TIME_MASK = ~WARM_META_MASK; // 0xFFFFFF80
|
||||
static constexpr uint32_t WARM_ROLE_MASK = 0x0Fu; // bits [3:0] device role (0..12)
|
||||
static constexpr uint32_t WARM_PROT_SHIFT = 4; // bits [5:4] protected category
|
||||
static constexpr uint32_t WARM_PROT_MASK = 0x03u;
|
||||
static constexpr uint32_t WARM_SIGNER_SHIFT = 6; // bit [6] we verified an XEdDSA signature from this node
|
||||
static constexpr uint32_t WARM_SIGNER_MASK = 0x01u;
|
||||
|
||||
// On-disk record format, from the page/file magic; older ones are normalised by load().
|
||||
enum class WarmFormat : uint8_t { Current, V2, V1 };
|
||||
|
||||
// Protected category cached alongside role so consumers needn't re-derive the mapping.
|
||||
enum class WarmProtected : uint8_t { None = 0, Role = 1, Flag = 2 };
|
||||
|
||||
inline uint32_t warmPackLastHeard(uint32_t lastHeard, uint8_t role, uint8_t prot)
|
||||
inline uint32_t warmPackLastHeard(uint32_t lastHeard, uint8_t role, uint8_t prot, bool signer)
|
||||
{
|
||||
return (lastHeard & WARM_TIME_MASK) | (static_cast<uint32_t>(role) & WARM_ROLE_MASK) |
|
||||
((static_cast<uint32_t>(prot) & WARM_PROT_MASK) << WARM_PROT_SHIFT);
|
||||
((static_cast<uint32_t>(prot) & WARM_PROT_MASK) << WARM_PROT_SHIFT) |
|
||||
((signer ? WARM_SIGNER_MASK : 0u) << WARM_SIGNER_SHIFT);
|
||||
}
|
||||
inline uint32_t warmTimeOf(const WarmNodeEntry &e)
|
||||
{
|
||||
@@ -77,6 +83,10 @@ inline uint8_t warmProtOf(const WarmNodeEntry &e)
|
||||
{
|
||||
return static_cast<uint8_t>((e.last_heard >> WARM_PROT_SHIFT) & WARM_PROT_MASK);
|
||||
}
|
||||
inline bool warmSignerOf(const WarmNodeEntry &e)
|
||||
{
|
||||
return ((e.last_heard >> WARM_SIGNER_SHIFT) & WARM_SIGNER_MASK) != 0;
|
||||
}
|
||||
|
||||
// Gated on NRF52840_XXAA: the ring sits at 0xEA000
|
||||
// valid only on the 1 MB-flash nRF52840.
|
||||
@@ -99,9 +109,11 @@ class WarmNodeStore
|
||||
/// entries; otherwise the oldest (keyless-first) entry is replaced.
|
||||
/// @param role the node's device role (meshtastic_Config_DeviceConfig_Role, 0..12)
|
||||
/// @param protectedCat WarmProtected category cached for the hop-trim path
|
||||
/// @param signer true if we ever verified an XEdDSA signature from this node, so
|
||||
/// re-admission restores the bit rather than relearning it
|
||||
/// @return true if the node was stored or updated
|
||||
bool absorb(NodeNum num, uint32_t lastHeard, const uint8_t *key32 /* may be NULL */, uint8_t role = 0,
|
||||
uint8_t protectedCat = 0);
|
||||
uint8_t protectedCat = 0, bool signer = false);
|
||||
|
||||
/// Look up the cached device role + protected category for a warm node.
|
||||
/// @return false if the node is not in the warm tier.
|
||||
@@ -167,7 +179,7 @@ class WarmNodeStore
|
||||
void ringAppend(const WarmNodeEntry &rec, int storeSlot /* -1 for tombstones */);
|
||||
void ringRotate(); // reclaim oldest page, compacting stranded live entries
|
||||
void ringOpenPage(uint8_t page); // erase + write header (seq = nextSeq++)
|
||||
bool ringReadHeader(uint8_t page, WarmPageHeader &h, bool *legacy = nullptr) const;
|
||||
bool ringReadHeader(uint8_t page, WarmPageHeader &h, WarmFormat *fmt = nullptr) const;
|
||||
#endif
|
||||
|
||||
bool save();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#include "mesh/eth/ethClient.h"
|
||||
#include "NodeDB.h"
|
||||
#include "RTC.h"
|
||||
#include "concurrency/Periodic.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "main.h"
|
||||
#include "mesh/api/ethServerAPI.h"
|
||||
#include "target_specific.h"
|
||||
|
||||
@@ -6,10 +6,34 @@
|
||||
#error Regenerate this file with the current version of nanopb generator.
|
||||
#endif
|
||||
|
||||
PB_BIND(meshtastic_SensorData, meshtastic_SensorData, AUTO)
|
||||
PB_BIND(meshtastic_FileTransfer, meshtastic_FileTransfer, 4)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_DirectoryListing, meshtastic_DirectoryListing, 4)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_I2CTransaction, meshtastic_I2CTransaction, 2)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_SdCardInfo, meshtastic_SdCardInfo, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_I2CResult, meshtastic_I2CResult, 2)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_InterdeviceMessage, meshtastic_InterdeviceMessage, 4)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
PB_BIND(meshtastic_InterdeviceMessage, meshtastic_InterdeviceMessage, 2)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -10,38 +10,194 @@
|
||||
#endif
|
||||
|
||||
/* Enum definitions */
|
||||
typedef enum _meshtastic_MessageType {
|
||||
meshtastic_MessageType_ACK = 0,
|
||||
meshtastic_MessageType_COLLECT_INTERVAL = 160, /* in ms */
|
||||
meshtastic_MessageType_BEEP_ON = 161, /* duration ms */
|
||||
meshtastic_MessageType_BEEP_OFF = 162, /* cancel prematurely */
|
||||
meshtastic_MessageType_SHUTDOWN = 163,
|
||||
meshtastic_MessageType_POWER_ON = 164,
|
||||
meshtastic_MessageType_SCD41_TEMP = 176,
|
||||
meshtastic_MessageType_SCD41_HUMIDITY = 177,
|
||||
meshtastic_MessageType_SCD41_CO2 = 178,
|
||||
meshtastic_MessageType_AHT20_TEMP = 179,
|
||||
meshtastic_MessageType_AHT20_HUMIDITY = 180,
|
||||
meshtastic_MessageType_TVOC_INDEX = 181
|
||||
} meshtastic_MessageType;
|
||||
/* Version of the interdevice protocol spoken on the link. Both sides send
|
||||
theirs in the ping/pong handshake; a peer reporting a different one runs
|
||||
firmware that does not match and is not talked to.
|
||||
|
||||
On a change that breaks the other side (renumbered fields, changed
|
||||
semantics, removed messages), raise the value of CURRENT. Do not add
|
||||
another entry: this enum carries a single constant, not a history. */
|
||||
typedef enum _meshtastic_InterdeviceVersion {
|
||||
meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_UNSPECIFIED = 0,
|
||||
/* Never use 1: ping/pong were bools before the handshake existed, and a
|
||||
bool true is the same varint on the wire as the number 1, so firmware
|
||||
predating the handshake would pass it. */
|
||||
meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_CURRENT = 2
|
||||
} meshtastic_InterdeviceVersion;
|
||||
|
||||
/* Defines the supported file operations */
|
||||
typedef enum _meshtastic_FileOperation {
|
||||
meshtastic_FileOperation_GET = 0,
|
||||
meshtastic_FileOperation_POST = 1,
|
||||
meshtastic_FileOperation_PUT = 2,
|
||||
meshtastic_FileOperation_DELETE = 3
|
||||
} meshtastic_FileOperation;
|
||||
|
||||
/* Outcome of a file or directory operation. The requester must be able to
|
||||
tell a transient condition from a definitive one: BUSY is worth another
|
||||
try, NOT_FOUND is not. */
|
||||
typedef enum _meshtastic_FileStatus {
|
||||
meshtastic_FileStatus_FILE_UNSPECIFIED = 0,
|
||||
meshtastic_FileStatus_FILE_OK = 1,
|
||||
/* Retry later: the co-processor is doing card maintenance (mount,
|
||||
free space scan) and cannot serve the request right now */
|
||||
meshtastic_FileStatus_FILE_BUSY = 2,
|
||||
meshtastic_FileStatus_FILE_NO_CARD = 3,
|
||||
meshtastic_FileStatus_FILE_NOT_FOUND = 4,
|
||||
/* PUT only: offset did not match the current end of the file. file_size
|
||||
carries the size the file actually has, so the writer can resync (or
|
||||
recognize its own chunk as already written after a lost response). */
|
||||
meshtastic_FileStatus_FILE_OFFSET_CONFLICT = 5,
|
||||
meshtastic_FileStatus_FILE_IO_ERROR = 6,
|
||||
meshtastic_FileStatus_FILE_NOT_A_FILE = 7 /* path is a directory (GET) or not one (listing) */
|
||||
} meshtastic_FileStatus;
|
||||
|
||||
/* What to do with the SD card of the co-processor */
|
||||
typedef enum _meshtastic_SdCommand {
|
||||
meshtastic_SdCommand_SD_COMMAND_UNSPECIFIED = 0,
|
||||
meshtastic_SdCommand_SD_MOUNT = 1, /* mount a card that is in the slot, also after an eject */
|
||||
meshtastic_SdCommand_SD_EJECT = 2, /* flush and release the card so it can be pulled safely */
|
||||
meshtastic_SdCommand_SD_FORMAT = 3 /* wipe the card and put a fresh FAT on it, then mount it */
|
||||
} meshtastic_SdCommand;
|
||||
|
||||
typedef enum _meshtastic_SdCardInfo_CardType {
|
||||
meshtastic_SdCardInfo_CardType_NONE = 0,
|
||||
meshtastic_SdCardInfo_CardType_MMC = 1,
|
||||
meshtastic_SdCardInfo_CardType_SD = 2,
|
||||
meshtastic_SdCardInfo_CardType_SDHC = 3,
|
||||
meshtastic_SdCardInfo_CardType_SDXC = 4,
|
||||
meshtastic_SdCardInfo_CardType_UNKNOWN_CARD = 5
|
||||
} meshtastic_SdCardInfo_CardType;
|
||||
|
||||
typedef enum _meshtastic_SdCardInfo_FatType {
|
||||
meshtastic_SdCardInfo_FatType_UNKNOWN_FAT = 0,
|
||||
meshtastic_SdCardInfo_FatType_FAT16 = 1,
|
||||
meshtastic_SdCardInfo_FatType_FAT32 = 2,
|
||||
meshtastic_SdCardInfo_FatType_EXFAT = 3
|
||||
} meshtastic_SdCardInfo_FatType;
|
||||
|
||||
typedef enum _meshtastic_I2CResult_Status {
|
||||
/* Never sent: an all-defaults (e.g. accidentally empty) message must
|
||||
not decode as a successful transaction */
|
||||
meshtastic_I2CResult_Status_UNSPECIFIED = 0,
|
||||
meshtastic_I2CResult_Status_OK = 1,
|
||||
meshtastic_I2CResult_Status_NACK_ADDRESS = 2,
|
||||
meshtastic_I2CResult_Status_NACK_DATA = 3,
|
||||
meshtastic_I2CResult_Status_ERROR = 4
|
||||
} meshtastic_I2CResult_Status;
|
||||
|
||||
/* Struct definitions */
|
||||
typedef struct _meshtastic_SensorData {
|
||||
/* The message type */
|
||||
meshtastic_MessageType type;
|
||||
pb_size_t which_data;
|
||||
union {
|
||||
float float_value;
|
||||
uint32_t uint32_value;
|
||||
} data;
|
||||
} meshtastic_SensorData;
|
||||
typedef PB_BYTES_ARRAY_T(4096) meshtastic_FileTransfer_filedata_t;
|
||||
/* Message for file operations */
|
||||
typedef struct _meshtastic_FileTransfer {
|
||||
meshtastic_FileOperation operation; /* File operation (GET, POST, PUT, DELETE) */
|
||||
char filepath[256]; /* Path of the file on the SD card */
|
||||
meshtastic_FileTransfer_filedata_t filedata; /* Chunk content (POST/PUT request, GET response) */
|
||||
meshtastic_FileStatus status; /* Response: outcome of the operation */
|
||||
char message[255]; /* Response: human readable detail, may be empty */
|
||||
uint64_t offset; /* Byte offset of this chunk within the file (ranged GET/PUT) */
|
||||
/* GET request: number of bytes to read, 0 = max chunk size. A response
|
||||
carries at most the filedata max_size (see interdevice.options) per
|
||||
chunk; larger requests are truncated, visible in the filedata length. */
|
||||
uint32_t length;
|
||||
uint64_t file_size; /* GET response: total size of the file */
|
||||
} meshtastic_FileTransfer;
|
||||
|
||||
/* Message for structured directory listing */
|
||||
typedef struct _meshtastic_DirectoryListing {
|
||||
char directory[256]; /* Path of the directory */
|
||||
/* One page of entry names, full FAT LFN length. Subdirectories carry a
|
||||
trailing slash. Note that a name whose directory prefix pushes the
|
||||
combined path past the FileTransfer.filepath limit cannot round-trip.
|
||||
Page size is the max_count in interdevice.options; page through with
|
||||
offset and total_count. */
|
||||
pb_size_t filenames_count;
|
||||
char filenames[16][256];
|
||||
meshtastic_FileStatus status; /* Response: outcome of the operation */
|
||||
char message[255]; /* Response: human readable detail, may be empty */
|
||||
uint32_t offset; /* Request: skip this many entries (paging) */
|
||||
uint32_t total_count; /* Response: total number of entries in the directory */
|
||||
} meshtastic_DirectoryListing;
|
||||
|
||||
typedef PB_BYTES_ARRAY_T(256) meshtastic_I2CTransaction_write_data_t;
|
||||
/* A single I2C transaction: an optional write followed by an optional
|
||||
read with repeated start, matching the TwoWire usage of sensor drivers
|
||||
(beginTransmission/write.../endTransmission(false)/requestFrom) */
|
||||
typedef struct _meshtastic_I2CTransaction {
|
||||
uint32_t address; /* 7-bit device address */
|
||||
meshtastic_I2CTransaction_write_data_t write_data; /* Bytes to write, may be empty */
|
||||
/* Number of bytes to read after the write, 0 = write-only. Bounded by
|
||||
the read_data max_size of I2CResult (see interdevice.options); larger
|
||||
requests are truncated, visible in the returned byte count. */
|
||||
uint32_t read_len;
|
||||
} meshtastic_I2CTransaction;
|
||||
|
||||
/* SD card statistics */
|
||||
typedef struct _meshtastic_SdCardInfo {
|
||||
/* Card initialized and usable. False while `busy` is set does not mean
|
||||
there is no card: the co-processor does not know yet. */
|
||||
bool present;
|
||||
meshtastic_SdCardInfo_CardType card_type;
|
||||
meshtastic_SdCardInfo_FatType fat_type;
|
||||
uint64_t card_size; /* Filesystem size in bytes */
|
||||
uint64_t used_bytes; /* Used bytes (may be expensive to compute on FAT32) */
|
||||
uint64_t free_bytes; /* Free bytes */
|
||||
/* used_bytes/free_bytes are only meaningful when true: the scan behind
|
||||
them runs in the background after mount and can take a while, and a
|
||||
full card is otherwise indistinguishable from a scan in progress */
|
||||
bool stats_valid;
|
||||
/* The co-processor is mounting a card right now, so whether one is
|
||||
present is not decided yet. Ask again rather than concluding the slot
|
||||
is empty. */
|
||||
bool busy;
|
||||
/* A card answers in the slot but carries no filesystem that could be
|
||||
mounted (present is false then). Formatting it makes it usable. */
|
||||
bool unformatted;
|
||||
} meshtastic_SdCardInfo;
|
||||
|
||||
typedef PB_BYTES_ARRAY_T(256) meshtastic_I2CResult_read_data_t;
|
||||
/* Result of an I2CTransaction */
|
||||
typedef struct _meshtastic_I2CResult {
|
||||
meshtastic_I2CResult_Status status;
|
||||
meshtastic_I2CResult_read_data_t read_data; /* Data read from the device, empty for write-only transactions */
|
||||
} meshtastic_I2CResult;
|
||||
|
||||
typedef PB_BYTES_ARRAY_T(128) meshtastic_InterdeviceMessage_i2c_scan_result_t;
|
||||
/* Main message for interdevice communication */
|
||||
typedef struct _meshtastic_InterdeviceMessage {
|
||||
pb_size_t which_data;
|
||||
union {
|
||||
char nmea[1024];
|
||||
meshtastic_SensorData sensor;
|
||||
uint16_t beep;
|
||||
meshtastic_I2CTransaction i2c_transaction;
|
||||
meshtastic_I2CResult i2c_result;
|
||||
bool i2c_scan; /* Request: scan the secondary I2C bus */
|
||||
meshtastic_InterdeviceMessage_i2c_scan_result_t i2c_scan_result; /* Response: 7-bit addresses of discovered devices */
|
||||
meshtastic_FileTransfer file_transfer;
|
||||
meshtastic_DirectoryListing directory_listing;
|
||||
bool get_sd_info; /* Request: SD card statistics */
|
||||
meshtastic_SdCardInfo sd_info; /* Response */
|
||||
/* Link liveness probe and version handshake. The receiver answers ping
|
||||
with pong, echoing the id. Touches no peripherals, so it works with
|
||||
nothing attached. Both carry the version the sender speaks; a peer
|
||||
that answers with a different one speaks another protocol and must
|
||||
not be used. */
|
||||
meshtastic_InterdeviceVersion ping;
|
||||
meshtastic_InterdeviceVersion pong;
|
||||
/* Response: the request could not be decoded or is of an unhandled
|
||||
type, so the requester fails fast instead of burning its timeout.
|
||||
Echoes the id when known, 0 when the frame was undecodable. Never
|
||||
sent in reaction to a nack. */
|
||||
bool nack;
|
||||
/* Request: mount the card, or release it so it can be pulled safely. The
|
||||
co-processor answers with sd_info. Without an eject the card is mounted
|
||||
on its own and kept mounted; after one it stays released until a mount
|
||||
is asked for. */
|
||||
meshtastic_SdCommand sd_command;
|
||||
} data;
|
||||
/* Correlates a response with its request: responses echo the id of the
|
||||
request they answer. 0 for unsolicited messages (e.g. the nmea stream). */
|
||||
uint32_t id;
|
||||
} meshtastic_InterdeviceMessage;
|
||||
|
||||
|
||||
@@ -50,53 +206,205 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
/* Helper constants for enums */
|
||||
#define _meshtastic_MessageType_MIN meshtastic_MessageType_ACK
|
||||
#define _meshtastic_MessageType_MAX meshtastic_MessageType_TVOC_INDEX
|
||||
#define _meshtastic_MessageType_ARRAYSIZE ((meshtastic_MessageType)(meshtastic_MessageType_TVOC_INDEX+1))
|
||||
#define _meshtastic_InterdeviceVersion_MIN meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_UNSPECIFIED
|
||||
#define _meshtastic_InterdeviceVersion_MAX meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_CURRENT
|
||||
#define _meshtastic_InterdeviceVersion_ARRAYSIZE ((meshtastic_InterdeviceVersion)(meshtastic_InterdeviceVersion_INTERDEVICE_VERSION_CURRENT+1))
|
||||
|
||||
#define meshtastic_SensorData_type_ENUMTYPE meshtastic_MessageType
|
||||
#define _meshtastic_FileOperation_MIN meshtastic_FileOperation_GET
|
||||
#define _meshtastic_FileOperation_MAX meshtastic_FileOperation_DELETE
|
||||
#define _meshtastic_FileOperation_ARRAYSIZE ((meshtastic_FileOperation)(meshtastic_FileOperation_DELETE+1))
|
||||
|
||||
#define _meshtastic_FileStatus_MIN meshtastic_FileStatus_FILE_UNSPECIFIED
|
||||
#define _meshtastic_FileStatus_MAX meshtastic_FileStatus_FILE_NOT_A_FILE
|
||||
#define _meshtastic_FileStatus_ARRAYSIZE ((meshtastic_FileStatus)(meshtastic_FileStatus_FILE_NOT_A_FILE+1))
|
||||
|
||||
#define _meshtastic_SdCommand_MIN meshtastic_SdCommand_SD_COMMAND_UNSPECIFIED
|
||||
#define _meshtastic_SdCommand_MAX meshtastic_SdCommand_SD_FORMAT
|
||||
#define _meshtastic_SdCommand_ARRAYSIZE ((meshtastic_SdCommand)(meshtastic_SdCommand_SD_FORMAT+1))
|
||||
|
||||
#define _meshtastic_SdCardInfo_CardType_MIN meshtastic_SdCardInfo_CardType_NONE
|
||||
#define _meshtastic_SdCardInfo_CardType_MAX meshtastic_SdCardInfo_CardType_UNKNOWN_CARD
|
||||
#define _meshtastic_SdCardInfo_CardType_ARRAYSIZE ((meshtastic_SdCardInfo_CardType)(meshtastic_SdCardInfo_CardType_UNKNOWN_CARD+1))
|
||||
|
||||
#define _meshtastic_SdCardInfo_FatType_MIN meshtastic_SdCardInfo_FatType_UNKNOWN_FAT
|
||||
#define _meshtastic_SdCardInfo_FatType_MAX meshtastic_SdCardInfo_FatType_EXFAT
|
||||
#define _meshtastic_SdCardInfo_FatType_ARRAYSIZE ((meshtastic_SdCardInfo_FatType)(meshtastic_SdCardInfo_FatType_EXFAT+1))
|
||||
|
||||
#define _meshtastic_I2CResult_Status_MIN meshtastic_I2CResult_Status_UNSPECIFIED
|
||||
#define _meshtastic_I2CResult_Status_MAX meshtastic_I2CResult_Status_ERROR
|
||||
#define _meshtastic_I2CResult_Status_ARRAYSIZE ((meshtastic_I2CResult_Status)(meshtastic_I2CResult_Status_ERROR+1))
|
||||
|
||||
#define meshtastic_FileTransfer_operation_ENUMTYPE meshtastic_FileOperation
|
||||
#define meshtastic_FileTransfer_status_ENUMTYPE meshtastic_FileStatus
|
||||
|
||||
#define meshtastic_DirectoryListing_status_ENUMTYPE meshtastic_FileStatus
|
||||
|
||||
|
||||
#define meshtastic_SdCardInfo_card_type_ENUMTYPE meshtastic_SdCardInfo_CardType
|
||||
#define meshtastic_SdCardInfo_fat_type_ENUMTYPE meshtastic_SdCardInfo_FatType
|
||||
|
||||
#define meshtastic_I2CResult_status_ENUMTYPE meshtastic_I2CResult_Status
|
||||
|
||||
#define meshtastic_InterdeviceMessage_data_ping_ENUMTYPE meshtastic_InterdeviceVersion
|
||||
#define meshtastic_InterdeviceMessage_data_pong_ENUMTYPE meshtastic_InterdeviceVersion
|
||||
#define meshtastic_InterdeviceMessage_data_sd_command_ENUMTYPE meshtastic_SdCommand
|
||||
|
||||
|
||||
/* Initializer values for message structs */
|
||||
#define meshtastic_SensorData_init_default {_meshtastic_MessageType_MIN, 0, {0}}
|
||||
#define meshtastic_InterdeviceMessage_init_default {0, {""}}
|
||||
#define meshtastic_SensorData_init_zero {_meshtastic_MessageType_MIN, 0, {0}}
|
||||
#define meshtastic_InterdeviceMessage_init_zero {0, {""}}
|
||||
#define meshtastic_FileTransfer_init_default {_meshtastic_FileOperation_MIN, "", {0, {0}}, _meshtastic_FileStatus_MIN, "", 0, 0, 0}
|
||||
#define meshtastic_DirectoryListing_init_default {"", 0, {"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""}, _meshtastic_FileStatus_MIN, "", 0, 0}
|
||||
#define meshtastic_I2CTransaction_init_default {0, {0, {0}}, 0}
|
||||
#define meshtastic_SdCardInfo_init_default {0, _meshtastic_SdCardInfo_CardType_MIN, _meshtastic_SdCardInfo_FatType_MIN, 0, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_I2CResult_init_default {_meshtastic_I2CResult_Status_MIN, {0, {0}}}
|
||||
#define meshtastic_InterdeviceMessage_init_default {0, {""}, 0}
|
||||
#define meshtastic_FileTransfer_init_zero {_meshtastic_FileOperation_MIN, "", {0, {0}}, _meshtastic_FileStatus_MIN, "", 0, 0, 0}
|
||||
#define meshtastic_DirectoryListing_init_zero {"", 0, {"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""}, _meshtastic_FileStatus_MIN, "", 0, 0}
|
||||
#define meshtastic_I2CTransaction_init_zero {0, {0, {0}}, 0}
|
||||
#define meshtastic_SdCardInfo_init_zero {0, _meshtastic_SdCardInfo_CardType_MIN, _meshtastic_SdCardInfo_FatType_MIN, 0, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_I2CResult_init_zero {_meshtastic_I2CResult_Status_MIN, {0, {0}}}
|
||||
#define meshtastic_InterdeviceMessage_init_zero {0, {""}, 0}
|
||||
|
||||
/* Field tags (for use in manual encoding/decoding) */
|
||||
#define meshtastic_SensorData_type_tag 1
|
||||
#define meshtastic_SensorData_float_value_tag 2
|
||||
#define meshtastic_SensorData_uint32_value_tag 3
|
||||
#define meshtastic_FileTransfer_operation_tag 1
|
||||
#define meshtastic_FileTransfer_filepath_tag 2
|
||||
#define meshtastic_FileTransfer_filedata_tag 3
|
||||
#define meshtastic_FileTransfer_status_tag 4
|
||||
#define meshtastic_FileTransfer_message_tag 5
|
||||
#define meshtastic_FileTransfer_offset_tag 6
|
||||
#define meshtastic_FileTransfer_length_tag 7
|
||||
#define meshtastic_FileTransfer_file_size_tag 8
|
||||
#define meshtastic_DirectoryListing_directory_tag 1
|
||||
#define meshtastic_DirectoryListing_filenames_tag 2
|
||||
#define meshtastic_DirectoryListing_status_tag 3
|
||||
#define meshtastic_DirectoryListing_message_tag 4
|
||||
#define meshtastic_DirectoryListing_offset_tag 5
|
||||
#define meshtastic_DirectoryListing_total_count_tag 6
|
||||
#define meshtastic_I2CTransaction_address_tag 1
|
||||
#define meshtastic_I2CTransaction_write_data_tag 2
|
||||
#define meshtastic_I2CTransaction_read_len_tag 3
|
||||
#define meshtastic_SdCardInfo_present_tag 1
|
||||
#define meshtastic_SdCardInfo_card_type_tag 2
|
||||
#define meshtastic_SdCardInfo_fat_type_tag 3
|
||||
#define meshtastic_SdCardInfo_card_size_tag 4
|
||||
#define meshtastic_SdCardInfo_used_bytes_tag 5
|
||||
#define meshtastic_SdCardInfo_free_bytes_tag 6
|
||||
#define meshtastic_SdCardInfo_stats_valid_tag 7
|
||||
#define meshtastic_SdCardInfo_busy_tag 8
|
||||
#define meshtastic_SdCardInfo_unformatted_tag 9
|
||||
#define meshtastic_I2CResult_status_tag 1
|
||||
#define meshtastic_I2CResult_read_data_tag 2
|
||||
#define meshtastic_InterdeviceMessage_nmea_tag 1
|
||||
#define meshtastic_InterdeviceMessage_sensor_tag 2
|
||||
#define meshtastic_InterdeviceMessage_beep_tag 2
|
||||
#define meshtastic_InterdeviceMessage_i2c_transaction_tag 3
|
||||
#define meshtastic_InterdeviceMessage_i2c_result_tag 4
|
||||
#define meshtastic_InterdeviceMessage_i2c_scan_tag 5
|
||||
#define meshtastic_InterdeviceMessage_i2c_scan_result_tag 6
|
||||
#define meshtastic_InterdeviceMessage_file_transfer_tag 7
|
||||
#define meshtastic_InterdeviceMessage_directory_listing_tag 8
|
||||
#define meshtastic_InterdeviceMessage_get_sd_info_tag 9
|
||||
#define meshtastic_InterdeviceMessage_sd_info_tag 10
|
||||
#define meshtastic_InterdeviceMessage_ping_tag 11
|
||||
#define meshtastic_InterdeviceMessage_pong_tag 12
|
||||
#define meshtastic_InterdeviceMessage_nack_tag 13
|
||||
#define meshtastic_InterdeviceMessage_sd_command_tag 14
|
||||
#define meshtastic_InterdeviceMessage_id_tag 15
|
||||
|
||||
/* Struct field encoding specification for nanopb */
|
||||
#define meshtastic_SensorData_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UENUM, type, 1) \
|
||||
X(a, STATIC, ONEOF, FLOAT, (data,float_value,data.float_value), 2) \
|
||||
X(a, STATIC, ONEOF, UINT32, (data,uint32_value,data.uint32_value), 3)
|
||||
#define meshtastic_SensorData_CALLBACK NULL
|
||||
#define meshtastic_SensorData_DEFAULT NULL
|
||||
#define meshtastic_FileTransfer_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UENUM, operation, 1) \
|
||||
X(a, STATIC, SINGULAR, STRING, filepath, 2) \
|
||||
X(a, STATIC, SINGULAR, BYTES, filedata, 3) \
|
||||
X(a, STATIC, SINGULAR, UENUM, status, 4) \
|
||||
X(a, STATIC, SINGULAR, STRING, message, 5) \
|
||||
X(a, STATIC, SINGULAR, UINT64, offset, 6) \
|
||||
X(a, STATIC, SINGULAR, UINT32, length, 7) \
|
||||
X(a, STATIC, SINGULAR, UINT64, file_size, 8)
|
||||
#define meshtastic_FileTransfer_CALLBACK NULL
|
||||
#define meshtastic_FileTransfer_DEFAULT NULL
|
||||
|
||||
#define meshtastic_DirectoryListing_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, STRING, directory, 1) \
|
||||
X(a, STATIC, REPEATED, STRING, filenames, 2) \
|
||||
X(a, STATIC, SINGULAR, UENUM, status, 3) \
|
||||
X(a, STATIC, SINGULAR, STRING, message, 4) \
|
||||
X(a, STATIC, SINGULAR, UINT32, offset, 5) \
|
||||
X(a, STATIC, SINGULAR, UINT32, total_count, 6)
|
||||
#define meshtastic_DirectoryListing_CALLBACK NULL
|
||||
#define meshtastic_DirectoryListing_DEFAULT NULL
|
||||
|
||||
#define meshtastic_I2CTransaction_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UINT32, address, 1) \
|
||||
X(a, STATIC, SINGULAR, BYTES, write_data, 2) \
|
||||
X(a, STATIC, SINGULAR, UINT32, read_len, 3)
|
||||
#define meshtastic_I2CTransaction_CALLBACK NULL
|
||||
#define meshtastic_I2CTransaction_DEFAULT NULL
|
||||
|
||||
#define meshtastic_SdCardInfo_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, BOOL, present, 1) \
|
||||
X(a, STATIC, SINGULAR, UENUM, card_type, 2) \
|
||||
X(a, STATIC, SINGULAR, UENUM, fat_type, 3) \
|
||||
X(a, STATIC, SINGULAR, UINT64, card_size, 4) \
|
||||
X(a, STATIC, SINGULAR, UINT64, used_bytes, 5) \
|
||||
X(a, STATIC, SINGULAR, UINT64, free_bytes, 6) \
|
||||
X(a, STATIC, SINGULAR, BOOL, stats_valid, 7) \
|
||||
X(a, STATIC, SINGULAR, BOOL, busy, 8) \
|
||||
X(a, STATIC, SINGULAR, BOOL, unformatted, 9)
|
||||
#define meshtastic_SdCardInfo_CALLBACK NULL
|
||||
#define meshtastic_SdCardInfo_DEFAULT NULL
|
||||
|
||||
#define meshtastic_I2CResult_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UENUM, status, 1) \
|
||||
X(a, STATIC, SINGULAR, BYTES, read_data, 2)
|
||||
#define meshtastic_I2CResult_CALLBACK NULL
|
||||
#define meshtastic_I2CResult_DEFAULT NULL
|
||||
|
||||
#define meshtastic_InterdeviceMessage_FIELDLIST(X, a) \
|
||||
X(a, STATIC, ONEOF, STRING, (data,nmea,data.nmea), 1) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (data,sensor,data.sensor), 2)
|
||||
X(a, STATIC, ONEOF, UINT32, (data,beep,data.beep), 2) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (data,i2c_transaction,data.i2c_transaction), 3) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (data,i2c_result,data.i2c_result), 4) \
|
||||
X(a, STATIC, ONEOF, BOOL, (data,i2c_scan,data.i2c_scan), 5) \
|
||||
X(a, STATIC, ONEOF, BYTES, (data,i2c_scan_result,data.i2c_scan_result), 6) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (data,file_transfer,data.file_transfer), 7) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (data,directory_listing,data.directory_listing), 8) \
|
||||
X(a, STATIC, ONEOF, BOOL, (data,get_sd_info,data.get_sd_info), 9) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (data,sd_info,data.sd_info), 10) \
|
||||
X(a, STATIC, ONEOF, UENUM, (data,ping,data.ping), 11) \
|
||||
X(a, STATIC, ONEOF, UENUM, (data,pong,data.pong), 12) \
|
||||
X(a, STATIC, ONEOF, BOOL, (data,nack,data.nack), 13) \
|
||||
X(a, STATIC, ONEOF, UENUM, (data,sd_command,data.sd_command), 14) \
|
||||
X(a, STATIC, SINGULAR, UINT32, id, 15)
|
||||
#define meshtastic_InterdeviceMessage_CALLBACK NULL
|
||||
#define meshtastic_InterdeviceMessage_DEFAULT NULL
|
||||
#define meshtastic_InterdeviceMessage_data_sensor_MSGTYPE meshtastic_SensorData
|
||||
#define meshtastic_InterdeviceMessage_data_i2c_transaction_MSGTYPE meshtastic_I2CTransaction
|
||||
#define meshtastic_InterdeviceMessage_data_i2c_result_MSGTYPE meshtastic_I2CResult
|
||||
#define meshtastic_InterdeviceMessage_data_file_transfer_MSGTYPE meshtastic_FileTransfer
|
||||
#define meshtastic_InterdeviceMessage_data_directory_listing_MSGTYPE meshtastic_DirectoryListing
|
||||
#define meshtastic_InterdeviceMessage_data_sd_info_MSGTYPE meshtastic_SdCardInfo
|
||||
|
||||
extern const pb_msgdesc_t meshtastic_SensorData_msg;
|
||||
extern const pb_msgdesc_t meshtastic_FileTransfer_msg;
|
||||
extern const pb_msgdesc_t meshtastic_DirectoryListing_msg;
|
||||
extern const pb_msgdesc_t meshtastic_I2CTransaction_msg;
|
||||
extern const pb_msgdesc_t meshtastic_SdCardInfo_msg;
|
||||
extern const pb_msgdesc_t meshtastic_I2CResult_msg;
|
||||
extern const pb_msgdesc_t meshtastic_InterdeviceMessage_msg;
|
||||
|
||||
/* Defines for backwards compatibility with code written before nanopb-0.4.0 */
|
||||
#define meshtastic_SensorData_fields &meshtastic_SensorData_msg
|
||||
#define meshtastic_FileTransfer_fields &meshtastic_FileTransfer_msg
|
||||
#define meshtastic_DirectoryListing_fields &meshtastic_DirectoryListing_msg
|
||||
#define meshtastic_I2CTransaction_fields &meshtastic_I2CTransaction_msg
|
||||
#define meshtastic_SdCardInfo_fields &meshtastic_SdCardInfo_msg
|
||||
#define meshtastic_I2CResult_fields &meshtastic_I2CResult_msg
|
||||
#define meshtastic_InterdeviceMessage_fields &meshtastic_InterdeviceMessage_msg
|
||||
|
||||
/* Maximum encoded size of messages (where known) */
|
||||
#define MESHTASTIC_MESHTASTIC_INTERDEVICE_PB_H_MAX_SIZE meshtastic_InterdeviceMessage_size
|
||||
#define meshtastic_InterdeviceMessage_size 1026
|
||||
#define meshtastic_SensorData_size 9
|
||||
#define meshtastic_DirectoryListing_size 4657
|
||||
#define meshtastic_FileTransfer_size 4646
|
||||
#define meshtastic_I2CResult_size 261
|
||||
#define meshtastic_I2CTransaction_size 271
|
||||
#define meshtastic_InterdeviceMessage_size 4666
|
||||
#define meshtastic_SdCardInfo_size 45
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
|
||||
@@ -121,7 +121,9 @@ typedef enum _meshtastic_TelemetrySensorType {
|
||||
/* ICM-42607-P 6‑Axis IMU */
|
||||
meshtastic_TelemetrySensorType_ICM42607P = 53,
|
||||
/* SPA06 pressure and temperature */
|
||||
meshtastic_TelemetrySensorType_SPA06 = 54
|
||||
meshtastic_TelemetrySensorType_SPA06 = 54,
|
||||
/* HM330X PM SENSOR */
|
||||
meshtastic_TelemetrySensorType_HM330X = 55
|
||||
} meshtastic_TelemetrySensorType;
|
||||
|
||||
/* Struct definitions */
|
||||
@@ -502,8 +504,8 @@ extern "C" {
|
||||
|
||||
/* Helper constants for enums */
|
||||
#define _meshtastic_TelemetrySensorType_MIN meshtastic_TelemetrySensorType_SENSOR_UNSET
|
||||
#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_SPA06
|
||||
#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_SPA06+1))
|
||||
#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_HM330X
|
||||
#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_HM330X+1))
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -96,14 +96,6 @@ static void taskCreateCert(void *parameter)
|
||||
{
|
||||
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");
|
||||
|
||||
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
|
||||
* Trigger : WebGui(SAVE)->WebServcice->phoneApi
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#include "configuration.h"
|
||||
#if HAS_WIFI
|
||||
#include "NodeDB.h"
|
||||
#include "RTC.h"
|
||||
#include "concurrency/Periodic.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "mesh/wifi/WiFiAPClient.h"
|
||||
|
||||
#include "main.h"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "meshUtils.h"
|
||||
#include "target_specific.h"
|
||||
#include <string.h>
|
||||
|
||||
/*
|
||||
@@ -79,6 +80,20 @@ bool memfll(const uint8_t *mem, uint8_t find, size_t numbytes)
|
||||
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, ...)
|
||||
{
|
||||
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?
|
||||
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, ...);
|
||||
|
||||
const std::string vformat(const char *const zcFormat, ...);
|
||||
|
||||
+164
-23
@@ -1,15 +1,17 @@
|
||||
#include "AdminModule.h"
|
||||
#include "Channels.h"
|
||||
#include "DisplayFormatters.h"
|
||||
#include "HardwareRNG.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PositionPrecision.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "SPILock.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "input/InputBroker.h"
|
||||
#include "meshUtils.h"
|
||||
#include <FSCommon.h>
|
||||
#include <Throttle.h>
|
||||
#include <ctype.h> // for better whitespace handling
|
||||
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI
|
||||
#include "MeshtasticOTA.h"
|
||||
@@ -48,6 +50,9 @@
|
||||
#include "GPS.h"
|
||||
#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
|
||||
#include "modules/PositionModule.h"
|
||||
#endif
|
||||
@@ -125,9 +130,16 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
||||
}
|
||||
#endif
|
||||
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)) {
|
||||
// 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");
|
||||
} else if (mp.from == 0) {
|
||||
// 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: {
|
||||
LOG_INFO("Client received a get_module_config response");
|
||||
if (fromOthers && r->get_module_config_response.which_payload_variant ==
|
||||
meshtastic_AdminMessage_ModuleConfigType_REMOTEHARDWARE_CONFIG) {
|
||||
// which_payload_variant is the ModuleConfig oneof tag, so compare against that tag, not the
|
||||
// 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);
|
||||
}
|
||||
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)
|
||||
{
|
||||
auto changes = SEGMENT_CONFIG;
|
||||
@@ -788,12 +822,8 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers)
|
||||
config.has_device = true;
|
||||
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && \
|
||||
!MESHTASTIC_EXCLUDE_ACCELEROMETER
|
||||
if (config.device.double_tap_as_button_press == false && c.payload_variant.device.double_tap_as_button_press == true &&
|
||||
accelerometerThread->enabled == false) {
|
||||
config.device.double_tap_as_button_press = c.payload_variant.device.double_tap_as_button_press;
|
||||
accelerometerThread->enabled = true;
|
||||
accelerometerThread->start();
|
||||
}
|
||||
reconcileAccelerometerThread(config.device.double_tap_as_button_press,
|
||||
c.payload_variant.device.double_tap_as_button_press, config.display.wake_on_tap_or_motion);
|
||||
#endif
|
||||
if (config.device.button_gpio == c.payload_variant.device.button_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 && \
|
||||
!MESHTASTIC_EXCLUDE_ACCELEROMETER
|
||||
if (config.display.wake_on_tap_or_motion == false && c.payload_variant.display.wake_on_tap_or_motion == true &&
|
||||
accelerometerThread->enabled == false) {
|
||||
config.display.wake_on_tap_or_motion = c.payload_variant.display.wake_on_tap_or_motion;
|
||||
accelerometerThread->enabled = true;
|
||||
accelerometerThread->start();
|
||||
}
|
||||
reconcileAccelerometerThread(config.display.wake_on_tap_or_motion, c.payload_variant.display.wake_on_tap_or_motion,
|
||||
config.device.double_tap_as_button_press);
|
||||
#endif
|
||||
config.display = c.payload_variant.display;
|
||||
break;
|
||||
@@ -1408,6 +1434,15 @@ void AdminModule::handleGetConfig(const meshtastic_MeshPacket &req, const uint32
|
||||
LOG_INFO("Get config: Security");
|
||||
res.get_config_response.which_payload_variant = meshtastic_Config_security_tag;
|
||||
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;
|
||||
case meshtastic_AdminMessage_ConfigType_SESSIONKEY_CONFIG:
|
||||
LOG_INFO("Get config: Sessionkey");
|
||||
@@ -1774,11 +1809,15 @@ AdminModule::AdminModule() : ProtobufModule("Admin", meshtastic_PortNum_ADMIN_AP
|
||||
|
||||
void AdminModule::setPassKey(meshtastic_AdminMessage *res)
|
||||
{
|
||||
if (session_time == 0 || millis() / 1000 > session_time + 150) {
|
||||
for (int i = 0; i < 8; i++) {
|
||||
session_passkey[i] = random();
|
||||
}
|
||||
session_time = millis() / 1000;
|
||||
// Regenerate once there is no session yet or the current key is older than 150s. session_time
|
||||
// holds millis(); the Throttle check is rollover-safe, unlike the previous seconds comparison.
|
||||
if (!sessionPasskeyValid || !Throttle::isWithinTimespanMs(session_time, 150 * 1000UL)) {
|
||||
// Session passkey authenticates admin replies, so it must be unpredictable: prefer the
|
||||
// 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);
|
||||
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
|
||||
printBytes("Incoming session key: ", res->session_passkey.bytes, 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);
|
||||
}
|
||||
|
||||
@@ -1812,6 +1852,107 @@ bool AdminModule::messageIsResponse(const meshtastic_AdminMessage *r)
|
||||
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)
|
||||
{
|
||||
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;
|
||||
|
||||
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);
|
||||
|
||||
@@ -77,7 +78,29 @@ class AdminModule : public ProtobufModule<meshtastic_AdminMessage>, public Obser
|
||||
public:
|
||||
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:
|
||||
// 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 handleSendInputEvent(const meshtastic_AdminMessage_InputEvent &inputEvent);
|
||||
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
|
||||
std::vector<String> seenChannels;
|
||||
seenChannels.reserve(channels.getNumChannels());
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
#include "ExternalNotificationModule.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "buzz/buzz.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "main.h"
|
||||
#include "mesh/generated/meshtastic/rtttl.pb.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
#if !MESHTASTIC_EXCLUDE_PKI
|
||||
#include "KeyVerificationModule.h"
|
||||
#include "CryptoEngine.h"
|
||||
#include "HardwareRNG.h"
|
||||
#include "MeshService.h"
|
||||
#include "RTC.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "graphics/draw/MenuHandler.h"
|
||||
#include "main.h"
|
||||
#include "meshUtils.h"
|
||||
#include "modules/AdminModule.h"
|
||||
#include "modules/NodeInfoModule.h"
|
||||
#include <RNG.h>
|
||||
#include <SHA256.h>
|
||||
|
||||
KeyVerificationModule *keyVerificationModule;
|
||||
@@ -34,7 +38,7 @@ AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(cons
|
||||
{
|
||||
updateState();
|
||||
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 &&
|
||||
currentState == KEY_VERIFICATION_IDLE) {
|
||||
@@ -48,9 +52,7 @@ AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(cons
|
||||
|
||||
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_VERIFY &&
|
||||
request->key_verification.nonce == currentNonce) {
|
||||
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
||||
if (remoteNodePtr)
|
||||
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||
commitVerifiedRemoteNode();
|
||||
resetToIdle();
|
||||
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY) {
|
||||
resetToIdle();
|
||||
@@ -63,9 +65,8 @@ AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(cons
|
||||
bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_KeyVerification *r)
|
||||
{
|
||||
updateState();
|
||||
if (mp.pki_encrypted == false) {
|
||||
return false;
|
||||
}
|
||||
// Note: pki_encrypted is not required here. The first response (M2) may arrive channel-encrypted in
|
||||
// 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()
|
||||
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 &&
|
||||
r->hash1.size == 0) {
|
||||
r->hash1.size == 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);
|
||||
});)
|
||||
|
||||
@@ -95,7 +101,8 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
|
||||
currentState = KEY_VERIFICATION_SENDER_AWAITING_NUMBER;
|
||||
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) {
|
||||
memset(message, 0, sizeof(message));
|
||||
sprintf(message, "Verification: \n");
|
||||
@@ -108,10 +115,9 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
|
||||
options.notificationType = graphics::notificationTypeEnum::selection_picker;
|
||||
options.bannerCallback =
|
||||
[=](int selected) {
|
||||
LOG_DEBUG("User selected %d for key verification", selected);
|
||||
if (selected == 1) {
|
||||
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
||||
if (remoteNodePtr)
|
||||
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||
keyVerificationModule->commitVerifiedRemoteNode();
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(options);)
|
||||
@@ -139,22 +145,29 @@ bool KeyVerificationModule::sendInitialRequest(NodeNum remoteNode)
|
||||
{
|
||||
LOG_DEBUG("keyVerification start");
|
||||
// generate nonce
|
||||
updateState();
|
||||
updateState(false);
|
||||
if (currentState != KEY_VERIFICATION_IDLE) {
|
||||
IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::ThrottleMessage;)
|
||||
return false;
|
||||
}
|
||||
updateState(true);
|
||||
currentNonce = random();
|
||||
currentNonceTimestamp = getTime();
|
||||
currentRemoteNode = remoteNode;
|
||||
meshtastic_KeyVerification KeyVerification = meshtastic_KeyVerification_init_zero;
|
||||
KeyVerification.nonce = currentNonce;
|
||||
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);
|
||||
p->to = remoteNode;
|
||||
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->priority = meshtastic_MeshPacket_Priority_HIGH;
|
||||
service->sendToMesh(p, RX_SRC_LOCAL, true);
|
||||
@@ -171,9 +184,6 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
|
||||
if (currentState != KEY_VERIFICATION_IDLE) { // TODO: cooldown period
|
||||
LOG_WARN("Key Verification requested, but already in a request");
|
||||
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;
|
||||
|
||||
@@ -188,15 +198,43 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
|
||||
response.nonce = scratch.nonce;
|
||||
currentRemoteNode = req.from;
|
||||
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.update(¤tSecurityNumber, sizeof(currentSecurityNumber));
|
||||
hash.update(¤tNonce, sizeof(currentNonce));
|
||||
hash.update(¤tRemoteNode, sizeof(currentRemoteNode));
|
||||
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.finalize(hash1, 32);
|
||||
|
||||
@@ -205,15 +243,19 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
|
||||
hash.update(¤tNonce, sizeof(currentNonce));
|
||||
hash.update(hash1, 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;
|
||||
memcpy(response.hash2.bytes, hash2, 32);
|
||||
|
||||
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);
|
||||
screen->showSimpleBanner(message, 30000); LOG_WARN("%s", message);)
|
||||
screen->showSimpleBanner(message, 30000); LOG_DEBUG("%s", message);)
|
||||
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
||||
if (cn) {
|
||||
cn->level = meshtastic_LogRecord_Level_WARNING;
|
||||
@@ -227,7 +269,7 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
|
||||
cn->payload_variant.key_verification_number_inform.security_number = currentSecurityNumber;
|
||||
service->sendClientNotification(cn);
|
||||
}
|
||||
LOG_WARN("Security Number %04u, nonce %llu", currentSecurityNumber, currentNonce);
|
||||
LOG_DEBUG("Security Number %04u, nonce %llu", currentSecurityNumber, currentNonce);
|
||||
return responsePacket;
|
||||
}
|
||||
|
||||
@@ -236,14 +278,18 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
|
||||
SHA256 hash;
|
||||
NodeNum ourNodeNum = nodeDB->getNodeNum();
|
||||
uint8_t scratch_hash[32] = {0};
|
||||
LOG_WARN("received security number: %u", incomingNumber);
|
||||
meshtastic_NodeInfoLite *remoteNodePtr = nullptr;
|
||||
remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
||||
if (!remoteNodePtr || !nodeInfoLiteHasUser(remoteNodePtr) || remoteNodePtr->public_key.size != 32) {
|
||||
currentState = KEY_VERIFICATION_IDLE;
|
||||
return; // should we throw an error here?
|
||||
LOG_DEBUG("received security number: %u", incomingNumber);
|
||||
meshtastic_NodeInfoLite *remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
||||
// Resolve the remote public key: NodeDB if known, otherwise the pending key learned during this
|
||||
// handshake (bootstrap case).
|
||||
meshtastic_NodeInfoLite_public_key_t remotePublic = {0, {0}};
|
||||
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
|
||||
hash.reset();
|
||||
hash.update(&incomingNumber, sizeof(incomingNumber));
|
||||
@@ -252,7 +298,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
|
||||
hash.update(¤tRemoteNode, sizeof(currentRemoteNode));
|
||||
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.reset();
|
||||
@@ -297,13 +343,13 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
|
||||
return;
|
||||
}
|
||||
|
||||
void KeyVerificationModule::updateState()
|
||||
void KeyVerificationModule::updateState(bool resetTimer)
|
||||
{
|
||||
if (currentState != KEY_VERIFICATION_IDLE) {
|
||||
// check for the 60 second timeout
|
||||
if (currentNonceTimestamp < getTime() - 60) {
|
||||
resetToIdle();
|
||||
} else {
|
||||
} else if (resetTimer) {
|
||||
currentNonceTimestamp = getTime();
|
||||
}
|
||||
}
|
||||
@@ -318,6 +364,32 @@ void KeyVerificationModule::resetToIdle()
|
||||
currentSecurityNumber = 0;
|
||||
currentRemoteNode = 0;
|
||||
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)
|
||||
|
||||
@@ -12,6 +12,39 @@ enum KeyVerificationState {
|
||||
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 //
|
||||
{
|
||||
// CallbackObserver<KeyVerificationModule, const meshtastic::Status *> nodeStatusObserver =
|
||||
@@ -29,6 +62,7 @@ class KeyVerificationModule : public ProtobufModule<meshtastic_KeyVerification>
|
||||
bool sendInitialRequest(NodeNum remoteNode);
|
||||
void generateVerificationCode(char *); // fills char with the user readable verification code
|
||||
uint32_t getCurrentRemoteNode() { return currentRemoteNode; }
|
||||
void commitVerifiedRemoteNode(); // Commit a pending key to NodeDB and mark the node manually verified
|
||||
|
||||
protected:
|
||||
/* Called to handle a particular incoming message
|
||||
@@ -58,8 +92,8 @@ class KeyVerificationModule : public ProtobufModule<meshtastic_KeyVerification>
|
||||
char message[40] = {0};
|
||||
|
||||
void processSecurityNumber(uint32_t);
|
||||
void updateState(); // check the timeouts and maybe reset the state to idle
|
||||
void resetToIdle(); // Zero out module state
|
||||
void updateState(bool resetTimer = true); // check the timeouts and maybe reset the state to idle
|
||||
void resetToIdle(); // Zero out module state
|
||||
};
|
||||
|
||||
extern KeyVerificationModule *keyVerificationModule;
|
||||
@@ -2,11 +2,11 @@
|
||||
#include "Default.h"
|
||||
#include "DisplayFormatters.h"
|
||||
#include "NodeDB.h"
|
||||
#include "RTC.h"
|
||||
#include "RadioInterface.h"
|
||||
#include "Router.h"
|
||||
#include "TransmitHistory.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "main.h"
|
||||
#include <Throttle.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
#if !MESHTASTIC_EXCLUDE_CANNEDMESSAGES
|
||||
#include "modules/CannedMessageModule.h"
|
||||
#endif
|
||||
#if HAS_SCREEN && BASEUI_HAS_GAMES
|
||||
#include "modules/games/GamesModule.h"
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_DETECTIONSENSOR
|
||||
#include "modules/DetectionSensorModule.h"
|
||||
#endif
|
||||
@@ -206,6 +209,11 @@ void setupModules()
|
||||
cannedMessageModule = new CannedMessageModule();
|
||||
}
|
||||
#endif
|
||||
#if HAS_SCREEN && BASEUI_HAS_GAMES
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
gamesModule = new GamesModule();
|
||||
}
|
||||
#endif
|
||||
#if ARCH_PORTDUINO
|
||||
new HostMetricsModule();
|
||||
#endif
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include "Default.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "RTC.h"
|
||||
#include "gps/RTC.h"
|
||||
#include <Throttle.h>
|
||||
|
||||
NeighborInfoModule *neighborInfoModule;
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "NodeStatus.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "TransmitHistory.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "main.h"
|
||||
#include <Throttle.h>
|
||||
#include <algorithm>
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PositionPrecision.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "TransmitHistory.h"
|
||||
#include "TypeConversions.h"
|
||||
#include "airtime.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "main.h"
|
||||
#include "meshUtils.h"
|
||||
#include "meshtastic/atak.pb.h"
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerMon.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "main.h"
|
||||
#include "sleep.h"
|
||||
#include "target_specific.h"
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "SPILock.h"
|
||||
#include "airtime.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
#include "gps/RTC.h"
|
||||
#include <Arduino.h>
|
||||
#include <Throttle.h>
|
||||
|
||||
@@ -154,7 +154,7 @@ ProcessMessage RangeTestModuleRadio::handleReceived(const meshtastic_MeshPacket
|
||||
NodeInfoLite *n = nodeDB->getMeshNode(getFrom(&mp));
|
||||
|
||||
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("---- Received Packet:");
|
||||
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));
|
||||
/*
|
||||
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("---- Received Packet:");
|
||||
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
|
||||
|
||||
// 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.flush();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#include "RemoteHardwareModule.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "main.h"
|
||||
#include <Throttle.h>
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
#include "MeshService.h"
|
||||
#include "NMEAWPL.h"
|
||||
#include "NodeDB.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/RTC.h"
|
||||
#include <Arduino.h>
|
||||
#include <Throttle.h>
|
||||
|
||||
@@ -396,7 +396,7 @@ ProcessMessage SerialModuleRadio::handleReceived(const meshtastic_MeshPacket &mp
|
||||
lastRxID = mp.id;
|
||||
// LOG_DEBUG("* * 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 {
|
||||
@@ -408,7 +408,7 @@ ProcessMessage SerialModuleRadio::handleReceived(const meshtastic_MeshPacket &mp
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp));
|
||||
const char *sender = nodeInfoLiteHasUser(node) ? node->short_name : "???";
|
||||
serialPrint->println();
|
||||
serialPrint->printf("%s: %s", sender, p.payload.bytes);
|
||||
serialPrint->printf("%s: %.*s", sender, (int)p.payload.size, p.payload.bytes);
|
||||
serialPrint->println();
|
||||
} else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA ||
|
||||
moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO) &&
|
||||
|
||||
@@ -15,11 +15,11 @@
|
||||
#include "StoreForwardModule.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "Throttle.h"
|
||||
#include "airtime.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "memGet.h"
|
||||
#include "mesh-pb-constants.h"
|
||||
#include "mesh/generated/meshtastic/storeforward.pb.h"
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "TransmitHistory.h"
|
||||
#include "UnitConversions.h"
|
||||
#include "detect/ScanI2CTwoWire.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/images.h"
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "RadioLibInterface.h"
|
||||
#include "Router.h"
|
||||
#include "TransmitHistory.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "main.h"
|
||||
#include "memGet.h"
|
||||
#include <OLEDDisplay.h>
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
#include "NodeDB.h"
|
||||
#include "Power.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "TransmitHistory.h"
|
||||
#include "UnitConversions.h"
|
||||
#include "buzz.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/images.h"
|
||||
#include "main.h"
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
#include "NodeDB.h"
|
||||
#include "Power.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "TransmitHistory.h"
|
||||
#include "UnitConversions.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "main.h"
|
||||
#include "sleep.h"
|
||||
#include "target_specific.h"
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
#include "Power.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "PowerTelemetry.h"
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "TransmitHistory.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "main.h"
|
||||
#include "sleep.h"
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
#include "../detect/ReClockI2C.h"
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "RTC.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include "gps/RTC.h"
|
||||
|
||||
#define PMSA003I_I2C_CLOCK_SPEED 100000
|
||||
#define PMSA003I_FRAME_LENGTH 32
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
#include "../detect/ReClockI2C.h"
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "RTC.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include "gps/RTC.h"
|
||||
#include <SensirionI2cScd4x.h>
|
||||
|
||||
// Max speed 400kHz
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
#include "../detect/ReClockI2C.h"
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "RTC.h"
|
||||
#include "TelemetrySensor.h"
|
||||
#include "Wire.h"
|
||||
#include "gps/RTC.h"
|
||||
|
||||
// Warm up times for SEN5X from the datasheet
|
||||
#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