mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-07 04:30:04 +10:00
* feat: add Nordic nRF54L15-DK variant (Zephyr + BLE + LoRa)
Adds a community hardware variant for the Nordic nRF54L15-DK (PCA10156)
with an external EBYTE E22-900M30S (SX1262) LoRa module. First Meshtastic
port running on the Zephyr RTOS; all other Nordic targets use the nRF5
SoftDevice stack.
Scope
-----
- New Zephyr-based platform layer under src/platform/nrf54l15/ providing
Arduino-compatible shims (Arduino.h, SPI, Wire, Print, Stream) over the
Zephyr APIs plus a LittleFS-backed InternalFileSystem on SPIM20.
- Bluetooth LE peripheral (NRF54L15Bluetooth.*) built on the Zephyr BT
host stack, exposing the Meshtastic GATT service with legacy
connectable advertising, just-works pairing, dynamic MTU exchange
(up to 247 bytes), and iOS connection-parameter tweaks.
- Variant directory variants/nrf54l15/nrf54l15dk/ with pin map for the
E22 module on connector J1, PlatformIO env (nrf54l15dk), Zephyr
DT overlay and a wiring README.
- Zephyr project config (zephyr/prj.conf + board overlay) tuned for
BT + LoRa: 16 KB main stack, 4 KB BT RX thread, RTT logging in
immediate mode, newlib-nano heap sized to leave room for the GATT
pools while still allowing ATT MTU=247.
- extra_scripts/nrf54l15_linker.py works around a PlatformIO + old Ninja
issue where Zephyr's two-pass linker script generation does not run
automatically; the post-script parses build.ninja and invokes the
gcc -E step directly before the final link.
- boards/nrf54l15dk.json board definition (PlatformIO needs it for the
DK; the Seeed platform only ships the XIAO variants).
- variants/rp2350/rp2350.ini excludes platform/nrf54l15/ from RP2350
build_src_filter so the shared platform tree does not leak between
targets.
- .gitignore: add nRF J-Link / RTT debug artifacts (flash.jlink,
rtt_*.txt).
Shared source changes
---------------------
- src/main.{cpp,h}, src/RedirectablePrint.cpp, src/FSCommon.{cpp,h},
src/mesh/{Channels,NodeDB,RadioLibInterface,MeshService,PhoneAPI}.cpp,
src/mesh/RadioLibInterface.h, src/modules/AdminModule.cpp: add small
guards / helpers so the Zephyr build compiles alongside the Arduino
targets. Behavior on existing boards is unchanged.
Hardware model
--------------
HW_VENDOR maps to meshtastic_HardwareModel_PRIVATE_HW until a dedicated
protobuf enum value is assigned upstream. The variant declares
custom_meshtastic_hw_model = 132 so the maintainers can wire the new
enum value through the protobufs repo after merge.
Hardware note
-------------
The E22-900M30S does not connect its DIO2 pin to TXEN internally — a
wire/solder bridge between DIO2 and TXEN on the module is required for
TX to work. Details and full pin map are in the variant README.
Validation
----------
Built clean against develop. On real hardware (April 2026) the port
passes end-to-end: iOS companion app pairs and connects, configuration
round-trip works, LoRa TX/RX reaches a canonical tbeam on the same mesh
channel, NodeDB updates propagate both ways, and traceroute completes.
* fix(nrf54l15): use atomic fs_rename instead of copy fallback
Zephyr LittleFS on nrf54l15 supports fs_rename natively, so route it
through the same atomic path as ESP32. The previous copyFile+remove
fallback truncated the destination before copying, leaving 0-byte files
if interrupted mid-write.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(nrf54l15): expand storage_partition from 36KB to 700KB
LittleFS on the default 9-block (36KB) storage_partition ran out of
space during copy-on-write of config.proto, causing fs_write to return
ENOSPC and pb_encode to surface "io error" when saving configuration
via the mobile app.
Reclaim slot1_partition (the MCUboot secondary slot — unused since we
flash directly via J-Link) and grow storage_partition to span
0xb6000..0x165000 (~175 blocks).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(nrf54l15): drop USERPREFS_LORACONFIG_* so LoRa config stays mutable
NodeDB rewrites LoRa config from USERPREFS_LORACONFIG_* on every boot,
which prevented reconfiguration via the BLE/serial app. Drop the
variant-level defaults; users configure region and modem preset through
the app like every other variant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(nrf54l15): enforce MITM passkey pairing on GATT service
- Add MESH_PERM_READ/MESH_PERM_WRITE macros (READ_AUTHEN/WRITE_AUTHEN)
on all mesh service characteristics so clients must complete passkey
exchange before accessing fromNum/fromRadio/toRadio/logRadio.
- Wire FIXED_PIN mode to bt_passkey_set() so the device advertises a
known PIN (config.bluetooth.fixed_pin); RANDOM_PIN keeps default
per-pairing random passkey.
- Reduce BleDeferredThread HARD_WATCHDOG_MS from 3min to 1min.
- prj.conf: CONFIG_BT_SMP_ENFORCE_MITM=y, CONFIG_BT_FIXED_PASSKEY=y,
CONFIG_BT_SMP_SC_PAIR_ONLY=n (legacy fallback for clients that abort
SC pairing with reason 0x01 within 150ms).
* fix(nrf54l15): resolve develop-merge conflict + cppcheck warnings
The `Merge branch 'develop'` left two ~RadioLibInterface() declarations
in src/mesh/RadioLibInterface.h: the inline version added upstream by
PR #10254 (which independently applied the same UAF guard this PR was
carrying) and the out-of-line version this PR introduced. GCC rejects
the duplicate, breaking every platform build. Drop the out-of-line
declaration + definition; keep upstream's inline form.
Also silence the 13 cppcheck low warnings introduced by the new
nrf54l15 Arduino shim — Arduino's `String`/`SPISettings` API contract
relies on implicit single-arg constructors used pervasively by
existing Meshtastic code, so suppress `noExplicitConstructor` inline
with a comment instead of breaking the API. The few mechanical wins
(`const tmp[2]`, `const uint32_t *sp`) are applied directly.
* fmt: fix Trunk Check lint issues on nrf54l15-port
- extra_scripts/nrf54l15_linker.py: move regular imports above
Import("env") to silence E402, add trunk-ignore-all(F821) for the
PIO/SCons SConstruct injection (matches esp32_pre.py / nrf52_extra.py
convention)
- src/platform/nrf54l15/NRF54L15Bluetooth.cpp: clang-format 16.0.3
- boards/nrf54l15dk.json + variants/nrf54l15/nrf54l15dk/README.md:
prettier 3.8.3 (also resolves markdownlint MD060 on README tables)
No behavior change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(nrf54l15): address Copilot review comments + correct clang-format style
Six review threads from the 2026-04-30 Copilot review:
- src/platform/nrf54l15/nrf54l15_main.cpp: validate PSP against the nRF54L15
SRAM range (0x20000000..0x20040000) and 4-byte alignment before walking the
faulting thread's stack, and clamp the walk so it never reads past the end
of RAM. Prevents a second fault inside the fatal handler when PSP is
corrupted (common in real faults).
- src/platform/nrf54l15/nrf54l15_arduino.cpp: gate the bring-up printk traces
in digitalWrite/digitalRead (CS/NRESET toggle log, BUSY-before-NRESET
snapshot, BUSY periodic timeline) behind a new -DNRF54L15_GPIO_DEBUG flag
that is off by default. The "dev NOT READY" message stays unconditional —
it indicates a genuine hardware/DTS misconfig.
- src/modules/AdminModule.cpp: don't mutate config.device.output_gpio_enabled
from handleGetConfig(). Reflect the live pin state in the response payload
only — a getter must not write back to disk-persisted state.
- src/platform/nrf54l15/InternalFileSystem.h: derive totalBytes() from
FIXED_PARTITION_SIZE(storage_partition) at compile time so it tracks the
DK overlay's ~700 KB partition instead of the stale 36 KB hard-coded value.
Updated the file header comment accordingly.
- extra_scripts/nrf54l15_linker.py: make _extract_gcc_command() handle the
POSIX Ninja COMMAND format (no `cmd.exe /C "..."` wrapper) in addition to
the Windows form, so the script doesn't hard-fail on Linux/macOS hosts.
- src/platform/nrf54l15/NRF54L15Bluetooth.cpp: clamp NO_PIN to RANDOM_PIN
with a one-shot LOG_WARN. The mesh GATT permissions are declared with
BT_GATT_PERM_*_AUTHEN and prj.conf sets CONFIG_BT_SMP_ENFORCE_MITM=y, so
NO_PIN with no auth callbacks would leave every characteristic returning
BT_ATT_ERR_AUTHENTICATION. Falling back to RANDOM_PIN keeps the link
usable instead of silently broken. Also re-formatted this file with the
project's .trunk/configs/.clang-format (Linux braces, 4-space indent,
130-col) — the previous lint-fix commit a2aca3234 accidentally used the
default LLVM style here, which CI's clang-format would have rejected.
Build verified: pio run -e nrf54l15dk passes, RAM 47.4%, Flash 28.6%.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(nrf54l15): address remaining Copilot review threads
Round 2/3 review fixes — bugs first, then docs/portability:
BLE concurrency (NRF54L15Bluetooth.cpp):
- onNowHasData / sendLog / BleDeferredThread / shutdown: acquire
active_conn under ble_mutex via new acquire_active_conn() helper so
disconnected_cb can't free the conn between the null check and
bt_conn_ref/bt_gatt_notify (use-after-unref).
- write_toradio: reject writes that exceed MAX_TO_FROM_RADIO_SIZE with
ATT_ERR_INVALID_ATTRIBUTE_LEN instead of returning success and silently
dropping the payload (would hide failed config writes from the phone).
- start_advertising: truncate the device name to fit the 31-byte legacy
scan-response limit and switch to BT_DATA_NAME_SHORTENED so
bt_le_adv_start() doesn't reject the payload when the name approaches
CONFIG_BT_DEVICE_NAME_MAX=32.
Linker / portability:
- main.h: drop the rp2040Loop() forward declaration that had no
definition and no callers — would surface as a link error if any RP2040
build added a call to the symbol.
- nrf54l15_arduino.cpp: transfer16() now uses static __aligned(4) DMA
buffers (matching transfer()), removing the EasyDMA-reachability hazard
of caller-stack buffers on this part.
Filesystem (InternalFileSystem.h):
- usedBytes(): return real usage from fs_statvfs() instead of 0 so OTA
/ range-test free-space guards work.
- rewindDirectory(): close the dir before reopening — Zephyr fs_dir_t has
no rewind, and re-fs_opendir on an open handle leaks LittleFS state.
Crash handler (nrf54l15_main.cpp):
- After the stack walk, busy-wait 50 ms to flush RTT/printk and call
sys_reboot(SYS_REBOOT_COLD) directly so the saved_crash record is
actually reported on the next boot. Default Zephyr config has
RESET_ON_FATAL_ERROR=n, so the previous k_fatal_halt() spun forever.
Generalization / config:
- PhoneAPI.cpp: replace the NRF54L15_DK ifdef with a
MESHTASTIC_EXCLUDE_FILES_MANIFEST capability flag (defined in the
nrf54l15dk env) so future variants can opt in/out without touching
shared code.
- variants/nrf54l15/nrf54l15.ini: parameterize libdeps include paths via
${PIOENV} so additional nRF54L15 envs sharing nrf54l15_base don't break.
- prj.conf: drop the stale "36 KB storage_partition" comments — the DK
overlay reclaims slot1 to ~700 KB and runtime size comes from
FIXED_PARTITION_SIZE.
- nrf54l15dk overlay: remove the zephyr,console / zephyr,shell-uart
chosen entries that conflicted with CONFIG_UART_CONSOLE=n + RTT
console; keep uart30 enabled so swapping the console is one Kconfig
flip away.
Build: nrf54l15dk SUCCESS (flash 28.6%, RAM 47.4%); wiznet_5500_evb_pico2
SUCCESS (verifies the shared main.h change).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(nrf54l15dk): use PRIVATE_HW (255) for custom_meshtastic_hw_model
Per @thebentern's review: the nRF54L15-DK is a development kit, not a
canonical Meshtastic SKU, so it falls under HardwareModel::PRIVATE_HW
(255) — the same enum value already used at runtime via HW_VENDOR. The
placeholder 132 is removed; no dedicated enum number will be assigned
for DK boards. Slug stays NRF54L15_DK as a human-readable identifier.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(nrf54l15): unstarve bt_long_wq so SC pairing completes
bt_pub_key_gen() runs the ECC P256 key generation on bt_long_wq. At default
prio=10 (preemptible) and stack=1400 it gets starved by Meshtastic app
threads at boot — sc_public_key stays NULL for minutes, smp_public_key()
defers with SMP_FLAG_PKEY_SEND, and every SC pairing attempt stalls right
after the public-key exchange. iOS shows "Connecting…" forever with no
PIN prompt; bleak/CLI fails the first CCC notify write with
"Protocol Error 0x05: Insufficient Authentication".
Set CONFIG_BT_LONG_WQ_PRIO=0 (highest preemptible, ties with main) and
CONFIG_BT_LONG_WQ_STACK_SIZE=4096 (margin for the P256M driver frames).
Validated E2E with iOS Meshtastic app: bt_smp_pkey_ready fires within ~40 s
of boot, 20 SC Passkey Entry rounds complete with matching pcnf/cfm,
encrypt 0x01 / sec_level 0x04 (Authenticated MITM), bonded=1.
* feat(nrf54l15): hardware I2C bus via TWIM30 + sensor telemetry
Adds the Arduino TwoWire layer for the nRF54L15-DK so Meshtastic's
sensor drivers can talk to external I2C devices over the hardware
TWIM30 peripheral.
Bus binding:
- &uart30 disabled in the board overlay (peripheral instance 30 is
shared between UARTE30 / TWIM30 / SPIM30 — pick one). Console stays
on RTT via CONFIG_RTT_CONSOLE.
- New i2c30_default / i2c30_sleep pinctrl with SDA=P0.03 / SCL=P0.04.
External 4.7 kOhm pull-ups required on both lines.
- &i2c30 enabled at I2C_BITRATE_FAST (400 kHz).
- button_3 (SW3, P0.04) deleted from DTS so the pad can be claimed by
i2c30 pinctrl; SW3 is still wired to the pad on the DK, do not press
it during I2C use or it will short SCL to GND.
Arduino layer:
- src/platform/nrf54l15/Wire.cpp resolves the DT node at compile time
via DEVICE_DT_GET(DT_NODELABEL(i2c30)) and dispatches Arduino's
beginTransmission / write / endTransmission / requestFrom to
i2c_write / i2c_write_read / i2c_read. Buffer is sized to 256 bytes
for forward compatibility with the SE050 secure element on the
custom PCB.
- Wire.h drops the prior compile-only stubs and exposes the real
TwoWire surface.
- Arduino.h: BitOrder becomes an enum (not #define) so Adafruit_BusIO's
`typedef BitOrder BusIOBitOrder;` compiles.
Variant + build flags:
- nrf54l15.ini flips HAS_WIRE / HAS_SENSOR / HAS_TELEMETRY from 0 to 1
and cherry-picks the sensor libs Meshtastic needs (BusIO, Sensor,
BMP280, BME280, INA219/226/260/3221, SHT4X). The full
environmental_base group is avoided because it pulls
Adafruit_SSD1306 / Adafruit_GFX which rely on Arduino pin macros the
Zephyr shim does not implement.
- nrf54l15dk variant.h defines PIN_WIRE_SDA / PIN_WIRE_SCL for parity
with the Arduino convention used by other variants. The actual bus
wiring is fixed by the overlay pinctrl above.
Validated 2026-05-14/15 on the DK with BMP280 @ 0x76 (temperature +
barometric pressure) and INA3221 @ 0x42 (rail voltage / current);
EnvironmentTelemetry / PowerTelemetry packets transmit successfully
over LoRa.
Footprint cost on nrf54l15dk: +45 KB flash, +1.7 KB RAM.
* feat(nodedb): honor USERPREFS for environment telemetry on first boot
installDefaultConfig() now respects two new compile-time prefs:
USERPREFS_CONFIG_ENV_TELEM_UPDATE_INTERVAL
USERPREFS_CONFIG_ENVIRONMENT_MEASUREMENT_ENABLED
The mobile apps enforce a 30 min floor on environment_update_interval
in the settings UI, which makes short-interval bring-up testing of new
sensor hardware painful — you have to wait half an hour for the first
LoRa packet to confirm wiring + driver. With these prefs baked into
the variant, the firmware can ship a freshly-flashed device that
broadcasts on a shorter cadence (e.g. 900 s) the moment storage_partition
is empty.
Both prefs are gated on #ifdef so the behavior is unchanged for any
variant that does not opt in. Documented in userPrefs.jsonc with the
existing telemetry-interval pref block.
* fix(nrf54l15): allow multiple bonded BLE peers
CONFIG_BT_MAX_PAIRED defaults to 1, so once the first peer (e.g. an
iOS phone) has paired and bonded, every subsequent pairing attempt
from a different MAC fails inside bt_keys_get_addr() with no free
key slot — the host returns BT_SECURITY_ERR_KEY_DOES_NOT_EXIST and
the second peer never gets past SMP.
Raise the slot count to 4 so the device can simultaneously hold an
iOS phone, a Windows host, a Linux host, and one spare bond. Add
BT_KEYS_OVERWRITE_OLDEST so that once the table fills, the LRU peer
is evicted on the next pairing rather than rejecting the new peer.
This matches the behavior other Meshtastic ports already provide
(nRF52 uses CONFIG_BT_PERIPHERAL_PRIO_CONN with similar semantics).
Discovered while bringing up the Python CLI on Windows alongside
the existing iOS bond.
* fix(nrf54l15): zero-initialize TwoWire buffers + clang-format Wire
cppcheck on every CI target (esp32s3, rp2040, rp2350, nrf52840, ...) was
failing the build with two `uninitMemberVar` warnings on TwoWire's
constructor: `txBuf` and `rxBuf` (256-byte arrays) were not initialized.
Even though the buffers are only read after txLen/rxLen is set, leaving
them uninitialized is a footgun if any future caller bypasses the
len-set step. Use C++11 value-initialization in the member initializer
list — costs ~512 B of memset at boot, gains a clean cppcheck pass and
defensive-against-future-changes semantics.
Also reformat Wire.{cpp,h} with the project's `.trunk/configs/.clang-format`
config so the Trunk Check Runner passes — clang-format moved the
`<errno.h>` include before the Zephyr-namespaced ones in Wire.cpp and
collapsed two inline overloads to single lines in Wire.h.
* fix(AdminModule): remove dead OUTPUT_GPIO_PIN/GpioOutputModule references
OUTPUT_GPIO_PIN is never defined and modules/GpioOutputModule.h doesn't
exist in the codebase; all #ifdef OUTPUT_GPIO_PIN branches were dead code
introduced by the nRF54L15-DK variant commit. Strips the include, the
output_gpio_enabled OFF→ON/ON→OFF transition logic in handleSetConfig(),
and the digitalRead() reflection in handleGetConfig().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
1263 lines
52 KiB
C++
1263 lines
52 KiB
C++
#include "configuration.h"
|
|
#if !MESHTASTIC_EXCLUDE_GPS
|
|
#include "GPS.h"
|
|
#endif
|
|
|
|
#include "Channels.h"
|
|
#include "Default.h"
|
|
#include "FSCommon.h"
|
|
#include "MeshService.h"
|
|
#include "NodeDB.h"
|
|
#include "PacketHistory.h"
|
|
#include "PhoneAPI.h"
|
|
#include "PowerFSM.h"
|
|
#include "RadioInterface.h"
|
|
#include "Router.h"
|
|
#include "SPILock.h"
|
|
#include "TypeConversions.h"
|
|
#include "concurrency/LockGuard.h"
|
|
#include "main.h"
|
|
#include "modules/NodeInfoModule.h"
|
|
#include "xmodem.h"
|
|
|
|
#if FromRadio_size > MAX_TO_FROM_RADIO_SIZE
|
|
#error FromRadio is too big
|
|
#endif
|
|
|
|
#if ToRadio_size > MAX_TO_FROM_RADIO_SIZE
|
|
#error ToRadio is too big
|
|
#endif
|
|
#if !MESHTASTIC_EXCLUDE_MQTT
|
|
#include "mqtt/MQTT.h"
|
|
#endif
|
|
#include "Throttle.h"
|
|
#include <RTC.h>
|
|
|
|
// Flag to indicate a heartbeat was received and we should send queue status
|
|
bool heartbeatReceived = false;
|
|
|
|
PhoneAPI::PhoneAPI()
|
|
{
|
|
lastContactMsec = millis();
|
|
std::fill(std::begin(recentToRadioPacketIds), std::end(recentToRadioPacketIds), 0);
|
|
}
|
|
|
|
PhoneAPI::~PhoneAPI()
|
|
{
|
|
close();
|
|
}
|
|
|
|
void PhoneAPI::handleStartConfig()
|
|
{
|
|
// Must be before setting state (because state is how we know !connected)
|
|
if (!isConnected()) {
|
|
onConnectionChanged(true);
|
|
observe(&service->fromNumChanged);
|
|
#ifdef FSCom
|
|
observe(&xModem.packetReady);
|
|
#endif
|
|
}
|
|
|
|
// Allow subclasses to prepare for high-throughput config traffic
|
|
onConfigStart();
|
|
|
|
// even if we were already connected - restart our state machine
|
|
if (config_nonce == SPECIAL_NONCE_ONLY_NODES) {
|
|
// If client only wants node info, jump directly to sending nodes
|
|
state = STATE_SEND_OWN_NODEINFO;
|
|
LOG_INFO("Client only wants node info, skipping other config");
|
|
} else {
|
|
state = STATE_SEND_MY_INFO;
|
|
}
|
|
pauseBluetoothLogging = true;
|
|
spiLock->lock();
|
|
#if defined(MESHTASTIC_EXCLUDE_FILES_MANIFEST)
|
|
// Skip the recursive FS walk. Used by platforms whose Zephyr LittleFS
|
|
// backend can't safely traverse a deep tree (e.g. nRF54L15) and platforms
|
|
// that don't support OTA browsing — the manifest is only consumed by
|
|
// companion apps for those flows.
|
|
filesManifest.clear();
|
|
#else
|
|
filesManifest = getFiles("/", 10);
|
|
#endif
|
|
spiLock->unlock();
|
|
LOG_DEBUG("Got %d files in manifest", filesManifest.size());
|
|
|
|
LOG_INFO("Start API client config millis=%u", millis());
|
|
// Protect against concurrent BLE callbacks: they run in NimBLE's FreeRTOS task and also touch nodeInfoQueue.
|
|
{
|
|
concurrency::LockGuard guard(&nodeInfoMutex);
|
|
nodeInfoForPhone = {};
|
|
nodeInfoQueue.clear();
|
|
replayQueue.clear();
|
|
replayPositionOrder.clear();
|
|
replayTelemetryOrder.clear();
|
|
replayEnvironmentOrder.clear();
|
|
replayStatusOrder.clear();
|
|
replayPositionIndex = 0;
|
|
replayTelemetryIndex = 0;
|
|
replayEnvironmentIndex = 0;
|
|
replayStatusIndex = 0;
|
|
}
|
|
resetReadIndex();
|
|
}
|
|
|
|
void PhoneAPI::close()
|
|
{
|
|
LOG_DEBUG("PhoneAPI::close()");
|
|
if (service->api_state == service->STATE_BLE && api_type == TYPE_BLE)
|
|
service->api_state = service->STATE_DISCONNECTED;
|
|
else if (service->api_state == service->STATE_WIFI && api_type == TYPE_WIFI)
|
|
service->api_state = service->STATE_DISCONNECTED;
|
|
else if (service->api_state == service->STATE_SERIAL && api_type == TYPE_SERIAL)
|
|
service->api_state = service->STATE_DISCONNECTED;
|
|
else if (service->api_state == service->STATE_PACKET && api_type == TYPE_PACKET)
|
|
service->api_state = service->STATE_DISCONNECTED;
|
|
else if (service->api_state == service->STATE_HTTP && api_type == TYPE_HTTP)
|
|
service->api_state = service->STATE_DISCONNECTED;
|
|
else if (service->api_state == service->STATE_ETH && api_type == TYPE_ETH)
|
|
service->api_state = service->STATE_DISCONNECTED;
|
|
|
|
if (state != STATE_SEND_NOTHING) {
|
|
state = STATE_SEND_NOTHING;
|
|
resetReadIndex();
|
|
unobserve(&service->fromNumChanged);
|
|
#ifdef FSCom
|
|
unobserve(&xModem.packetReady);
|
|
#endif
|
|
releasePhonePacket(); // Don't leak phone packets on shutdown
|
|
releaseQueueStatusPhonePacket();
|
|
releaseMqttClientProxyPhonePacket();
|
|
releaseClientNotification();
|
|
onConnectionChanged(false);
|
|
fromRadioScratch = {};
|
|
toRadioScratch = {};
|
|
// Clear cached node info under lock because NimBLE callbacks can still be draining it.
|
|
{
|
|
concurrency::LockGuard guard(&nodeInfoMutex);
|
|
nodeInfoForPhone = {};
|
|
nodeInfoQueue.clear();
|
|
replayQueue.clear();
|
|
replayPositionOrder.clear();
|
|
replayTelemetryOrder.clear();
|
|
replayEnvironmentOrder.clear();
|
|
replayStatusOrder.clear();
|
|
replayPositionIndex = 0;
|
|
replayTelemetryIndex = 0;
|
|
replayEnvironmentIndex = 0;
|
|
replayStatusIndex = 0;
|
|
replayPhase = REPLAY_PHASE_IDLE;
|
|
}
|
|
packetForPhone = NULL;
|
|
filesManifest.clear();
|
|
filesManifest.shrink_to_fit();
|
|
lastPortNumToRadio.clear();
|
|
fromRadioNum = 0;
|
|
config_nonce = 0;
|
|
config_state = 0;
|
|
pauseBluetoothLogging = false;
|
|
heartbeatReceived = false;
|
|
}
|
|
}
|
|
|
|
bool PhoneAPI::checkConnectionTimeout()
|
|
{
|
|
if (isConnected()) {
|
|
bool newContact = checkIsConnected();
|
|
if (!newContact) {
|
|
LOG_INFO("Lost phone connection");
|
|
close();
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Handle a ToRadio protobuf
|
|
*/
|
|
bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength)
|
|
{
|
|
powerFSM.trigger(EVENT_CONTACT_FROM_PHONE); // As long as the phone keeps talking to us, don't let the radio go to sleep
|
|
lastContactMsec = millis();
|
|
|
|
memset(&toRadioScratch, 0, sizeof(toRadioScratch));
|
|
if (pb_decode_from_bytes(buf, bufLength, &meshtastic_ToRadio_msg, &toRadioScratch)) {
|
|
switch (toRadioScratch.which_payload_variant) {
|
|
case meshtastic_ToRadio_packet_tag:
|
|
return handleToRadioPacket(toRadioScratch.packet);
|
|
case meshtastic_ToRadio_want_config_id_tag:
|
|
config_nonce = toRadioScratch.want_config_id;
|
|
LOG_INFO("Client wants config, nonce=%u", config_nonce);
|
|
handleStartConfig();
|
|
break;
|
|
case meshtastic_ToRadio_disconnect_tag:
|
|
LOG_INFO("Disconnect from phone");
|
|
close();
|
|
break;
|
|
case meshtastic_ToRadio_xmodemPacket_tag:
|
|
LOG_INFO("Got xmodem packet");
|
|
#ifdef FSCom
|
|
xModem.handlePacket(toRadioScratch.xmodemPacket);
|
|
#endif
|
|
break;
|
|
#if !MESHTASTIC_EXCLUDE_MQTT
|
|
case meshtastic_ToRadio_mqttClientProxyMessage_tag:
|
|
LOG_DEBUG("Got MqttClientProxy message");
|
|
if (state != STATE_SEND_PACKETS) {
|
|
LOG_WARN("Ignore MqttClientProxy message while completing config handshake");
|
|
break;
|
|
}
|
|
if (mqtt && moduleConfig.mqtt.proxy_to_client_enabled && moduleConfig.mqtt.enabled &&
|
|
(channels.anyMqttEnabled() || moduleConfig.mqtt.map_reporting_enabled)) {
|
|
mqtt->onClientProxyReceive(toRadioScratch.mqttClientProxyMessage);
|
|
} else {
|
|
LOG_WARN("MqttClientProxy received but proxy is not enabled, no channels have up/downlink, or map reporting "
|
|
"not enabled");
|
|
}
|
|
break;
|
|
#endif
|
|
case meshtastic_ToRadio_heartbeat_tag:
|
|
// nonce==1 is a special "nodeinfo ping" trigger: force a fresh
|
|
// NodeInfo broadcast on the 60-second shorterTimeout path so
|
|
// peers can re-learn our public key after a reboot or
|
|
// factory_reset without waiting out the normal 10-minute
|
|
// NodeInfo send cooldown. Mirrors the TCP/UDP path in
|
|
// `src/mesh/api/PacketAPI.cpp:74-79` for serial clients.
|
|
// Default nonce (0) remains a plain keepalive that triggers
|
|
// a queue-status reply.
|
|
if (toRadioScratch.heartbeat.nonce == 1) {
|
|
if (nodeInfoModule) {
|
|
LOG_INFO("Broadcasting nodeinfo ping (serial)");
|
|
nodeInfoModule->sendOurNodeInfo(NODENUM_BROADCAST, true, 0, true);
|
|
}
|
|
} else {
|
|
LOG_DEBUG("Got client heartbeat");
|
|
heartbeatReceived = true;
|
|
}
|
|
break;
|
|
default:
|
|
// Ignore nop messages
|
|
break;
|
|
}
|
|
} else {
|
|
LOG_ERROR("Error: ignore malformed toradio");
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get the next packet we want to send to the phone, or NULL if no such packet is available.
|
|
*
|
|
* We assume buf is at least FromRadio_size bytes long.
|
|
*
|
|
* Our sending states progress in the following sequence (the client apps ASSUME THIS SEQUENCE, DO NOT CHANGE IT):
|
|
STATE_SEND_MY_INFO, // send our my info record
|
|
STATE_SEND_UIDATA,
|
|
STATE_SEND_OWN_NODEINFO,
|
|
STATE_SEND_METADATA,
|
|
STATE_SEND_CHANNELS
|
|
STATE_SEND_CONFIG,
|
|
STATE_SEND_MODULE_CONFIG,
|
|
STATE_SEND_OTHER_NODEINFOS, // states progress in this order as the device sends to the client
|
|
STATE_SEND_FILEMANIFEST,
|
|
STATE_SEND_COMPLETE_ID,
|
|
STATE_SEND_PACKETS // send packets or debug strings
|
|
*/
|
|
|
|
size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
|
{
|
|
// Respond to heartbeat by sending queue status
|
|
if (heartbeatReceived) {
|
|
memset(&fromRadioScratch, 0, sizeof(fromRadioScratch));
|
|
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_queueStatus_tag;
|
|
fromRadioScratch.queueStatus = router->getQueueStatus();
|
|
heartbeatReceived = false;
|
|
size_t numbytes = pb_encode_to_bytes(buf, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratch);
|
|
LOG_DEBUG("FromRadio=STATE_SEND_QUEUE_STATUS, numbytes=%u", numbytes);
|
|
return numbytes;
|
|
}
|
|
|
|
if (!available()) {
|
|
return 0;
|
|
}
|
|
// In case we send a FromRadio packet
|
|
memset(&fromRadioScratch, 0, sizeof(fromRadioScratch));
|
|
|
|
// Advance states as needed
|
|
switch (state) {
|
|
case STATE_SEND_NOTHING:
|
|
LOG_DEBUG("FromRadio=STATE_SEND_NOTHING");
|
|
break;
|
|
case STATE_SEND_MY_INFO:
|
|
LOG_DEBUG("FromRadio=STATE_SEND_MY_INFO");
|
|
// If the user has specified they don't want our node to share its location, make sure to tell the phone
|
|
// app not to send locations on our behalf.
|
|
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_my_info_tag;
|
|
strncpy(myNodeInfo.pio_env, optstr(APP_ENV), sizeof(myNodeInfo.pio_env));
|
|
myNodeInfo.nodedb_count = static_cast<uint16_t>(nodeDB->getNumMeshNodes());
|
|
fromRadioScratch.my_info = myNodeInfo;
|
|
state = STATE_SEND_UIDATA;
|
|
|
|
service->refreshLocalMeshNode(); // Update my NodeInfo because the client will be asking for it soon.
|
|
break;
|
|
|
|
case STATE_SEND_UIDATA:
|
|
LOG_INFO("getFromRadio=STATE_SEND_UIDATA");
|
|
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_deviceuiConfig_tag;
|
|
fromRadioScratch.deviceuiConfig = uiconfig;
|
|
state = STATE_SEND_OWN_NODEINFO;
|
|
break;
|
|
|
|
case STATE_SEND_OWN_NODEINFO: {
|
|
LOG_DEBUG("Send My NodeInfo");
|
|
auto us = nodeDB->readNextMeshNode(readIndex);
|
|
if (us) {
|
|
auto info = TypeConversions::ConvertToNodeInfo(us);
|
|
info.has_hops_away = false;
|
|
info.is_favorite = true;
|
|
{
|
|
concurrency::LockGuard guard(&nodeInfoMutex);
|
|
nodeInfoForPhone = info;
|
|
}
|
|
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag;
|
|
fromRadioScratch.node_info = info;
|
|
// Should allow us to resume sending NodeInfo in STATE_SEND_OTHER_NODEINFOS
|
|
{
|
|
concurrency::LockGuard guard(&nodeInfoMutex);
|
|
nodeInfoForPhone.num = 0;
|
|
}
|
|
}
|
|
if (config_nonce == SPECIAL_NONCE_ONLY_NODES) {
|
|
// If client only wants node info, jump directly to sending nodes
|
|
state = STATE_SEND_OTHER_NODEINFOS;
|
|
onNowHasData(0);
|
|
} else {
|
|
state = STATE_SEND_METADATA;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case STATE_SEND_METADATA:
|
|
LOG_DEBUG("Send device metadata");
|
|
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_metadata_tag;
|
|
fromRadioScratch.metadata = getDeviceMetadata();
|
|
state = STATE_SEND_CHANNELS;
|
|
break;
|
|
|
|
case STATE_SEND_CHANNELS:
|
|
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_channel_tag;
|
|
fromRadioScratch.channel = channels.getByIndex(config_state);
|
|
config_state++;
|
|
// Advance when we have sent all of our Channels
|
|
if (config_state >= MAX_NUM_CHANNELS) {
|
|
LOG_DEBUG("Send channels %d", config_state);
|
|
state = STATE_SEND_CONFIG;
|
|
config_state = _meshtastic_AdminMessage_ConfigType_MIN + 1;
|
|
}
|
|
break;
|
|
|
|
case STATE_SEND_CONFIG:
|
|
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_config_tag;
|
|
switch (config_state) {
|
|
case meshtastic_Config_device_tag:
|
|
LOG_DEBUG("Send config: device");
|
|
fromRadioScratch.config.which_payload_variant = meshtastic_Config_device_tag;
|
|
fromRadioScratch.config.payload_variant.device = config.device;
|
|
break;
|
|
case meshtastic_Config_position_tag:
|
|
LOG_DEBUG("Send config: position");
|
|
fromRadioScratch.config.which_payload_variant = meshtastic_Config_position_tag;
|
|
fromRadioScratch.config.payload_variant.position = config.position;
|
|
break;
|
|
case meshtastic_Config_power_tag:
|
|
LOG_DEBUG("Send config: power");
|
|
fromRadioScratch.config.which_payload_variant = meshtastic_Config_power_tag;
|
|
fromRadioScratch.config.payload_variant.power = config.power;
|
|
fromRadioScratch.config.payload_variant.power.ls_secs = default_ls_secs;
|
|
break;
|
|
case meshtastic_Config_network_tag:
|
|
LOG_DEBUG("Send config: network");
|
|
fromRadioScratch.config.which_payload_variant = meshtastic_Config_network_tag;
|
|
fromRadioScratch.config.payload_variant.network = config.network;
|
|
break;
|
|
case meshtastic_Config_display_tag:
|
|
LOG_DEBUG("Send config: display");
|
|
fromRadioScratch.config.which_payload_variant = meshtastic_Config_display_tag;
|
|
fromRadioScratch.config.payload_variant.display = config.display;
|
|
break;
|
|
case meshtastic_Config_lora_tag:
|
|
LOG_DEBUG("Send config: lora");
|
|
fromRadioScratch.config.which_payload_variant = meshtastic_Config_lora_tag;
|
|
fromRadioScratch.config.payload_variant.lora = config.lora;
|
|
break;
|
|
case meshtastic_Config_bluetooth_tag:
|
|
LOG_DEBUG("Send config: bluetooth");
|
|
fromRadioScratch.config.which_payload_variant = meshtastic_Config_bluetooth_tag;
|
|
fromRadioScratch.config.payload_variant.bluetooth = config.bluetooth;
|
|
break;
|
|
case meshtastic_Config_security_tag:
|
|
LOG_DEBUG("Send config: security");
|
|
fromRadioScratch.config.which_payload_variant = meshtastic_Config_security_tag;
|
|
fromRadioScratch.config.payload_variant.security = config.security;
|
|
break;
|
|
case meshtastic_Config_sessionkey_tag:
|
|
LOG_DEBUG("Send config: sessionkey");
|
|
fromRadioScratch.config.which_payload_variant = meshtastic_Config_sessionkey_tag;
|
|
break;
|
|
case meshtastic_Config_device_ui_tag: // NOOP!
|
|
fromRadioScratch.config.which_payload_variant = meshtastic_Config_device_ui_tag;
|
|
break;
|
|
default:
|
|
LOG_ERROR("Unknown config type %d", config_state);
|
|
}
|
|
// NOTE: The phone app needs to know the ls_secs value so it can properly expect sleep behavior.
|
|
// So even if we internally use 0 to represent 'use default' we still need to send the value we are
|
|
// using to the app (so that even old phone apps work with new device loads).
|
|
|
|
config_state++;
|
|
// Advance when we have sent all of our config objects
|
|
if (config_state > (_meshtastic_AdminMessage_ConfigType_MAX + 1)) {
|
|
state = STATE_SEND_MODULECONFIG;
|
|
config_state = _meshtastic_AdminMessage_ModuleConfigType_MIN + 1;
|
|
}
|
|
break;
|
|
|
|
case STATE_SEND_MODULECONFIG:
|
|
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_moduleConfig_tag;
|
|
switch (config_state) {
|
|
case meshtastic_ModuleConfig_mqtt_tag:
|
|
LOG_DEBUG("Send module config: mqtt");
|
|
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_mqtt_tag;
|
|
fromRadioScratch.moduleConfig.payload_variant.mqtt = moduleConfig.mqtt;
|
|
break;
|
|
case meshtastic_ModuleConfig_serial_tag:
|
|
LOG_DEBUG("Send module config: serial");
|
|
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_serial_tag;
|
|
fromRadioScratch.moduleConfig.payload_variant.serial = moduleConfig.serial;
|
|
break;
|
|
case meshtastic_ModuleConfig_external_notification_tag:
|
|
LOG_DEBUG("Send module config: ext notification");
|
|
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_external_notification_tag;
|
|
fromRadioScratch.moduleConfig.payload_variant.external_notification = moduleConfig.external_notification;
|
|
break;
|
|
case meshtastic_ModuleConfig_store_forward_tag:
|
|
LOG_DEBUG("Send module config: store forward");
|
|
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_store_forward_tag;
|
|
fromRadioScratch.moduleConfig.payload_variant.store_forward = moduleConfig.store_forward;
|
|
break;
|
|
case meshtastic_ModuleConfig_range_test_tag:
|
|
LOG_DEBUG("Send module config: range test");
|
|
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_range_test_tag;
|
|
fromRadioScratch.moduleConfig.payload_variant.range_test = moduleConfig.range_test;
|
|
break;
|
|
case meshtastic_ModuleConfig_telemetry_tag:
|
|
LOG_DEBUG("Send module config: telemetry");
|
|
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_telemetry_tag;
|
|
fromRadioScratch.moduleConfig.payload_variant.telemetry = moduleConfig.telemetry;
|
|
break;
|
|
case meshtastic_ModuleConfig_canned_message_tag:
|
|
LOG_DEBUG("Send module config: canned message");
|
|
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_canned_message_tag;
|
|
fromRadioScratch.moduleConfig.payload_variant.canned_message = moduleConfig.canned_message;
|
|
break;
|
|
case meshtastic_ModuleConfig_audio_tag:
|
|
LOG_DEBUG("Send module config: audio");
|
|
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_audio_tag;
|
|
fromRadioScratch.moduleConfig.payload_variant.audio = moduleConfig.audio;
|
|
break;
|
|
case meshtastic_ModuleConfig_remote_hardware_tag:
|
|
LOG_DEBUG("Send module config: remote hardware");
|
|
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_remote_hardware_tag;
|
|
fromRadioScratch.moduleConfig.payload_variant.remote_hardware = moduleConfig.remote_hardware;
|
|
break;
|
|
case meshtastic_ModuleConfig_neighbor_info_tag:
|
|
LOG_DEBUG("Send module config: neighbor info");
|
|
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_neighbor_info_tag;
|
|
fromRadioScratch.moduleConfig.payload_variant.neighbor_info = moduleConfig.neighbor_info;
|
|
break;
|
|
case meshtastic_ModuleConfig_detection_sensor_tag:
|
|
LOG_DEBUG("Send module config: detection sensor");
|
|
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_detection_sensor_tag;
|
|
fromRadioScratch.moduleConfig.payload_variant.detection_sensor = moduleConfig.detection_sensor;
|
|
break;
|
|
case meshtastic_ModuleConfig_ambient_lighting_tag:
|
|
LOG_DEBUG("Send module config: ambient lighting");
|
|
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_ambient_lighting_tag;
|
|
fromRadioScratch.moduleConfig.payload_variant.ambient_lighting = moduleConfig.ambient_lighting;
|
|
break;
|
|
case meshtastic_ModuleConfig_paxcounter_tag:
|
|
LOG_DEBUG("Send module config: paxcounter");
|
|
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag;
|
|
fromRadioScratch.moduleConfig.payload_variant.paxcounter = moduleConfig.paxcounter;
|
|
break;
|
|
case meshtastic_ModuleConfig_traffic_management_tag:
|
|
LOG_DEBUG("Send module config: traffic management");
|
|
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_traffic_management_tag;
|
|
fromRadioScratch.moduleConfig.payload_variant.traffic_management = moduleConfig.traffic_management;
|
|
break;
|
|
case meshtastic_ModuleConfig_tak_tag:
|
|
LOG_DEBUG("Send module config: tak");
|
|
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_tak_tag;
|
|
fromRadioScratch.moduleConfig.payload_variant.tak = moduleConfig.tak;
|
|
break;
|
|
default:
|
|
LOG_DEBUG("Unhandled module config type %d", config_state);
|
|
}
|
|
|
|
config_state++;
|
|
// Advance when we have sent all of our ModuleConfig objects
|
|
if (config_state > (_meshtastic_AdminMessage_ModuleConfigType_MAX + 1)) {
|
|
// Handle special nonce behaviors:
|
|
// - SPECIAL_NONCE_ONLY_CONFIG: Skip node info, go directly to file manifest
|
|
// - SPECIAL_NONCE_ONLY_NODES: After sending nodes, skip to complete
|
|
if (config_nonce == SPECIAL_NONCE_ONLY_CONFIG) {
|
|
state = STATE_SEND_FILEMANIFEST;
|
|
} else {
|
|
state = STATE_SEND_OTHER_NODEINFOS;
|
|
onNowHasData(0);
|
|
}
|
|
config_state = 0;
|
|
}
|
|
break;
|
|
|
|
case STATE_SEND_OTHER_NODEINFOS: {
|
|
if (readIndex == 2) { // readIndex==2 will be true for the first non-us node
|
|
LOG_INFO("Start sending nodeinfos millis=%u", millis());
|
|
}
|
|
|
|
meshtastic_NodeInfo infoToSend = {};
|
|
{
|
|
concurrency::LockGuard guard(&nodeInfoMutex);
|
|
if (nodeInfoForPhone.num == 0 && !nodeInfoQueue.empty()) {
|
|
// Serve the next cached node without re-reading from the DB iterator.
|
|
nodeInfoForPhone = nodeInfoQueue.front();
|
|
nodeInfoQueue.pop_front();
|
|
}
|
|
infoToSend = nodeInfoForPhone;
|
|
if (infoToSend.num != 0)
|
|
nodeInfoForPhone = {};
|
|
}
|
|
|
|
if (infoToSend.num != 0) {
|
|
// Just in case we stored a different user.id in the past, but should never happen going forward
|
|
sprintf(infoToSend.user.id, "!%08x", infoToSend.num);
|
|
|
|
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag;
|
|
fromRadioScratch.node_info = infoToSend;
|
|
prefetchNodeInfos();
|
|
} else {
|
|
LOG_DEBUG("Done sending %d of %d nodeinfos millis=%u", readIndex, nodeDB->getNumMeshNodes(), millis());
|
|
nodeInfoMutex.lock();
|
|
nodeInfoQueue.clear();
|
|
nodeInfoMutex.unlock();
|
|
// Satellite-DB replay (positions/telemetry/environment/status) now happens
|
|
// *after* config_complete_id, interleaved with live traffic in STATE_SEND_PACKETS.
|
|
state = STATE_SEND_FILEMANIFEST;
|
|
return getFromRadio(buf);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case STATE_SEND_FILEMANIFEST: {
|
|
LOG_DEBUG("FromRadio=STATE_SEND_FILEMANIFEST");
|
|
// ONLY_NODES variants skip the manifest.
|
|
if (config_state == filesManifest.size() || config_nonce == SPECIAL_NONCE_ONLY_NODES) {
|
|
config_state = 0;
|
|
filesManifest.clear();
|
|
// Skip to complete packet
|
|
sendConfigComplete();
|
|
} else {
|
|
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_fileInfo_tag;
|
|
fromRadioScratch.fileInfo = filesManifest.at(config_state);
|
|
LOG_DEBUG("File: %s (%d) bytes", fromRadioScratch.fileInfo.file_name, fromRadioScratch.fileInfo.size_bytes);
|
|
config_state++;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case STATE_SEND_COMPLETE_ID:
|
|
sendConfigComplete();
|
|
break;
|
|
|
|
case STATE_SEND_PACKETS:
|
|
pauseBluetoothLogging = false;
|
|
// Do we have a message from the mesh or packet from the local device?
|
|
LOG_DEBUG("FromRadio=STATE_SEND_PACKETS");
|
|
if (queueStatusPacketForPhone) {
|
|
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_queueStatus_tag;
|
|
fromRadioScratch.queueStatus = *queueStatusPacketForPhone;
|
|
releaseQueueStatusPhonePacket();
|
|
} else if (mqttClientProxyMessageForPhone) {
|
|
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_mqttClientProxyMessage_tag;
|
|
fromRadioScratch.mqttClientProxyMessage = *mqttClientProxyMessageForPhone;
|
|
releaseMqttClientProxyPhonePacket();
|
|
} else if (xmodemPacketForPhone.control != meshtastic_XModem_Control_NUL) {
|
|
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_xmodemPacket_tag;
|
|
fromRadioScratch.xmodemPacket = xmodemPacketForPhone;
|
|
xmodemPacketForPhone = meshtastic_XModem_init_zero;
|
|
} else if (clientNotification) {
|
|
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_clientNotification_tag;
|
|
fromRadioScratch.clientNotification = *clientNotification;
|
|
releaseClientNotification();
|
|
} else if (packetForPhone) {
|
|
printPacket("phone downloaded packet", packetForPhone);
|
|
|
|
// Encapsulate as a FromRadio packet
|
|
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag;
|
|
fromRadioScratch.packet = *packetForPhone;
|
|
releasePhonePacket();
|
|
} else if (replayPending()) {
|
|
// No live packet pending — feed the phone one cached satellite-DB packet.
|
|
// popReplayPacket advances through positions->telemetry->environment->status,
|
|
// and flips replayPhase back to IDLE when everything has been drained.
|
|
meshtastic_MeshPacket replayPkt;
|
|
if (popReplayPacket(replayPkt)) {
|
|
printPacket("replay packet to phone", &replayPkt);
|
|
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag;
|
|
fromRadioScratch.packet = replayPkt;
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
LOG_ERROR("getFromRadio unexpected state %d", state);
|
|
}
|
|
|
|
// Do we have a message from the mesh?
|
|
if (fromRadioScratch.which_payload_variant != 0) {
|
|
// Encapsulate as a FromRadio packet
|
|
size_t numbytes = pb_encode_to_bytes(buf, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratch);
|
|
|
|
// VERY IMPORTANT to not print debug messages while writing to fromRadioScratch - because we use that same buffer
|
|
// for logging (when we are encapsulating with protobufs)
|
|
return numbytes;
|
|
}
|
|
|
|
LOG_DEBUG("No FromRadio packet available");
|
|
return 0;
|
|
}
|
|
|
|
void PhoneAPI::sendConfigComplete()
|
|
{
|
|
LOG_INFO("Config Send Complete millis=%u", millis());
|
|
const bool shouldReplaySatellites = (config_nonce != SPECIAL_NONCE_ONLY_CONFIG);
|
|
// The phone sees config_complete_id first (treats sync as done), then the cached
|
|
// satellite-DB packets (positions / telemetry / environment / status) trickle in
|
|
// afterward as ordinary mesh packets (except SPECIAL_NONCE_ONLY_CONFIG, which
|
|
// skips node/satellite sync entirely). Any client that handles live POSITION_APP /
|
|
// TELEMETRY_APP / NODE_STATUS_APP packets handles these identically. STM32WL and
|
|
// other builds that compile the satellite DBs out produce no replay packets and
|
|
// the phase advances to IDLE in microseconds.
|
|
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_config_complete_id_tag;
|
|
fromRadioScratch.config_complete_id = config_nonce;
|
|
config_nonce = 0;
|
|
state = STATE_SEND_PACKETS;
|
|
replayPhase = shouldReplaySatellites ? REPLAY_PHASE_POSITIONS : REPLAY_PHASE_IDLE;
|
|
if (api_type == TYPE_BLE) {
|
|
service->api_state = service->STATE_BLE;
|
|
} else if (api_type == TYPE_WIFI) {
|
|
service->api_state = service->STATE_WIFI;
|
|
} else if (api_type == TYPE_SERIAL) {
|
|
service->api_state = service->STATE_SERIAL;
|
|
} else if (api_type == TYPE_PACKET) {
|
|
service->api_state = service->STATE_PACKET;
|
|
} else if (api_type == TYPE_HTTP) {
|
|
service->api_state = service->STATE_HTTP;
|
|
} else if (api_type == TYPE_ETH) {
|
|
service->api_state = service->STATE_ETH;
|
|
}
|
|
|
|
// Allow subclasses to know we've entered steady-state so they can lower power consumption
|
|
onConfigComplete();
|
|
|
|
pauseBluetoothLogging = false;
|
|
}
|
|
|
|
void PhoneAPI::releasePhonePacket()
|
|
{
|
|
if (packetForPhone) {
|
|
service->releaseToPool(packetForPhone); // we just copied the bytes, so don't need this buffer anymore
|
|
packetForPhone = NULL;
|
|
}
|
|
}
|
|
|
|
void PhoneAPI::releaseQueueStatusPhonePacket()
|
|
{
|
|
if (queueStatusPacketForPhone) {
|
|
service->releaseQueueStatusToPool(queueStatusPacketForPhone);
|
|
queueStatusPacketForPhone = NULL;
|
|
}
|
|
}
|
|
|
|
void PhoneAPI::prefetchNodeInfos()
|
|
{
|
|
bool added = false;
|
|
bool wasEmpty = false;
|
|
// Other-node NodeInfos always go out thin (no bundled position/device_metrics).
|
|
// The post-config_complete_id replay drain delivers those as ordinary mesh packets.
|
|
// Keep the queue topped up so BLE reads stay responsive even if DB fetches take a moment.
|
|
{
|
|
concurrency::LockGuard guard(&nodeInfoMutex);
|
|
wasEmpty = nodeInfoQueue.empty();
|
|
while (nodeInfoQueue.size() < kNodePrefetchDepth) {
|
|
auto nextNode = nodeDB->readNextMeshNode(readIndex);
|
|
if (!nextNode)
|
|
break;
|
|
|
|
auto info = TypeConversions::ConvertToNodeInfoThin(nextNode);
|
|
bool isUs = info.num == nodeDB->getNodeNum();
|
|
info.hops_away = isUs ? 0 : info.hops_away;
|
|
info.last_heard = isUs ? getValidTime(RTCQualityFromNet) : info.last_heard;
|
|
info.snr = isUs ? 0 : info.snr;
|
|
info.via_mqtt = isUs ? false : info.via_mqtt;
|
|
info.is_favorite = info.is_favorite || isUs;
|
|
nodeInfoQueue.push_back(info);
|
|
// Log progress here (at fetch time) so readIndex is accurate and each value logs only once.
|
|
if (readIndex == 2 || readIndex % 20 == 0) {
|
|
LOG_DEBUG("nodeinfo: %d/%d", readIndex, nodeDB->getNumMeshNodes());
|
|
}
|
|
added = true;
|
|
}
|
|
}
|
|
|
|
if (added && wasEmpty)
|
|
onNowHasData(0);
|
|
}
|
|
|
|
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;
|
|
pkt.from = num;
|
|
pkt.to = NODENUM_BROADCAST;
|
|
pkt.id = generatePacketId();
|
|
pkt.rx_time = pos.time;
|
|
pkt.channel = 0;
|
|
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
|
pkt.hop_start = pkt.hop_limit;
|
|
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
|
// Mark as if heard over the air, not internally generated
|
|
pkt.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
|
|
pkt.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
pkt.decoded.portnum = meshtastic_PortNum_POSITION_APP;
|
|
meshtastic_Position fullPos = TypeConversions::ConvertToPosition(pos);
|
|
size_t len =
|
|
pb_encode_to_bytes(pkt.decoded.payload.bytes, sizeof(pkt.decoded.payload.bytes), &meshtastic_Position_msg, &fullPos);
|
|
pkt.decoded.payload.size = (pb_size_t)len;
|
|
return pkt;
|
|
}
|
|
|
|
meshtastic_MeshPacket PhoneAPI::makeReplayTelemetryPacket(NodeNum num, const meshtastic_DeviceMetrics &metrics)
|
|
{
|
|
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.channel = 0;
|
|
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
|
pkt.hop_start = pkt.hop_limit;
|
|
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.
|
|
pkt.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
|
|
pkt.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
pkt.decoded.portnum = meshtastic_PortNum_TELEMETRY_APP;
|
|
meshtastic_Telemetry fullTel = meshtastic_Telemetry_init_default;
|
|
fullTel.time = pkt.rx_time;
|
|
fullTel.which_variant = meshtastic_Telemetry_device_metrics_tag;
|
|
fullTel.variant.device_metrics = metrics;
|
|
size_t len =
|
|
pb_encode_to_bytes(pkt.decoded.payload.bytes, sizeof(pkt.decoded.payload.bytes), &meshtastic_Telemetry_msg, &fullTel);
|
|
pkt.decoded.payload.size = (pb_size_t)len;
|
|
return pkt;
|
|
}
|
|
|
|
void PhoneAPI::beginReplayPositions()
|
|
{
|
|
#if MESHTASTIC_EXCLUDE_POSITIONDB
|
|
// Build excluded entirely - leave the order list empty so the phase
|
|
// immediately drains and advances.
|
|
replayPositionOrder.clear();
|
|
replayPositionIndex = 0;
|
|
#else
|
|
// Caller (popReplayPacket) only invokes us when replayPhase is armed.
|
|
// Snapshot the keyset at phase start so concurrent inserts/erases on the
|
|
// map don't invalidate iteration. Skip our own node - the phone already
|
|
// got our position bundled in STATE_SEND_OWN_NODEINFO.
|
|
replayPositionOrder = nodeDB->snapshotPositionNodeNums(nodeDB->getNodeNum());
|
|
replayPositionIndex = 0;
|
|
LOG_INFO("Begin position replay: %u entries millis=%u", (unsigned)replayPositionOrder.size(), millis());
|
|
#endif
|
|
}
|
|
|
|
void PhoneAPI::prefetchReplayPositions()
|
|
{
|
|
#if MESHTASTIC_EXCLUDE_POSITIONDB
|
|
return;
|
|
#else
|
|
bool added = false;
|
|
bool wasEmpty = false;
|
|
{
|
|
concurrency::LockGuard guard(&nodeInfoMutex);
|
|
wasEmpty = replayQueue.empty();
|
|
while (replayQueue.size() < kReplayPrefetchDepth && replayPositionIndex < replayPositionOrder.size()) {
|
|
NodeNum num = replayPositionOrder[replayPositionIndex++];
|
|
meshtastic_PositionLite pos;
|
|
if (!nodeDB->copyNodePosition(num, pos))
|
|
continue; // entry was evicted between snapshot and now
|
|
replayQueue.push_back(makeReplayPositionPacket(num, pos));
|
|
added = true;
|
|
}
|
|
}
|
|
if (added && wasEmpty)
|
|
onNowHasData(0);
|
|
#endif
|
|
}
|
|
|
|
void PhoneAPI::beginReplayTelemetry()
|
|
{
|
|
#if MESHTASTIC_EXCLUDE_TELEMETRYDB
|
|
replayTelemetryOrder.clear();
|
|
replayTelemetryIndex = 0;
|
|
#else
|
|
replayTelemetryOrder = nodeDB->snapshotTelemetryNodeNums(nodeDB->getNodeNum());
|
|
replayTelemetryIndex = 0;
|
|
LOG_INFO("Begin telemetry replay: %u entries millis=%u", (unsigned)replayTelemetryOrder.size(), millis());
|
|
#endif
|
|
}
|
|
|
|
void PhoneAPI::prefetchReplayTelemetry()
|
|
{
|
|
#if MESHTASTIC_EXCLUDE_TELEMETRYDB
|
|
return;
|
|
#else
|
|
bool added = false;
|
|
bool wasEmpty = false;
|
|
{
|
|
concurrency::LockGuard guard(&nodeInfoMutex);
|
|
wasEmpty = replayQueue.empty();
|
|
while (replayQueue.size() < kReplayPrefetchDepth && replayTelemetryIndex < replayTelemetryOrder.size()) {
|
|
NodeNum num = replayTelemetryOrder[replayTelemetryIndex++];
|
|
meshtastic_DeviceMetrics dm;
|
|
if (!nodeDB->copyNodeTelemetry(num, dm))
|
|
continue;
|
|
replayQueue.push_back(makeReplayTelemetryPacket(num, dm));
|
|
added = true;
|
|
}
|
|
}
|
|
if (added && wasEmpty)
|
|
onNowHasData(0);
|
|
#endif
|
|
}
|
|
|
|
meshtastic_MeshPacket PhoneAPI::makeReplayEnvironmentPacket(uint32_t num, const meshtastic_EnvironmentMetrics &env)
|
|
{
|
|
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.channel = 0;
|
|
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
|
pkt.hop_start = pkt.hop_limit;
|
|
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.
|
|
pkt.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
|
|
pkt.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
pkt.decoded.portnum = meshtastic_PortNum_TELEMETRY_APP;
|
|
meshtastic_Telemetry fullTel = meshtastic_Telemetry_init_default;
|
|
fullTel.time = pkt.rx_time;
|
|
fullTel.which_variant = meshtastic_Telemetry_environment_metrics_tag;
|
|
fullTel.variant.environment_metrics = env;
|
|
size_t len =
|
|
pb_encode_to_bytes(pkt.decoded.payload.bytes, sizeof(pkt.decoded.payload.bytes), &meshtastic_Telemetry_msg, &fullTel);
|
|
pkt.decoded.payload.size = (pb_size_t)len;
|
|
return pkt;
|
|
}
|
|
|
|
void PhoneAPI::beginReplayEnvironment()
|
|
{
|
|
#if MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
|
replayEnvironmentOrder.clear();
|
|
replayEnvironmentIndex = 0;
|
|
#else
|
|
replayEnvironmentOrder = nodeDB->snapshotEnvironmentNodeNums(nodeDB->getNodeNum());
|
|
replayEnvironmentIndex = 0;
|
|
LOG_INFO("Begin environment replay: %u entries millis=%u", (unsigned)replayEnvironmentOrder.size(), millis());
|
|
#endif
|
|
}
|
|
|
|
void PhoneAPI::prefetchReplayEnvironment()
|
|
{
|
|
#if MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
|
return;
|
|
#else
|
|
bool added = false;
|
|
bool wasEmpty = false;
|
|
{
|
|
concurrency::LockGuard guard(&nodeInfoMutex);
|
|
wasEmpty = replayQueue.empty();
|
|
while (replayQueue.size() < kReplayPrefetchDepth && replayEnvironmentIndex < replayEnvironmentOrder.size()) {
|
|
NodeNum num = replayEnvironmentOrder[replayEnvironmentIndex++];
|
|
meshtastic_EnvironmentMetrics env;
|
|
if (!nodeDB->copyNodeEnvironment(num, env))
|
|
continue;
|
|
replayQueue.push_back(makeReplayEnvironmentPacket(num, env));
|
|
added = true;
|
|
}
|
|
}
|
|
if (added && wasEmpty)
|
|
onNowHasData(0);
|
|
#endif
|
|
}
|
|
|
|
meshtastic_MeshPacket PhoneAPI::makeReplayStatusPacket(uint32_t num, const meshtastic_StatusMessage &status)
|
|
{
|
|
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.channel = 0;
|
|
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
|
pkt.hop_start = pkt.hop_limit;
|
|
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;
|
|
pkt.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
pkt.decoded.portnum = meshtastic_PortNum_NODE_STATUS_APP;
|
|
size_t len =
|
|
pb_encode_to_bytes(pkt.decoded.payload.bytes, sizeof(pkt.decoded.payload.bytes), &meshtastic_StatusMessage_msg, &status);
|
|
pkt.decoded.payload.size = (pb_size_t)len;
|
|
return pkt;
|
|
}
|
|
|
|
void PhoneAPI::beginReplayStatus()
|
|
{
|
|
#if MESHTASTIC_EXCLUDE_STATUSDB
|
|
replayStatusOrder.clear();
|
|
replayStatusIndex = 0;
|
|
#else
|
|
replayStatusOrder = nodeDB->snapshotStatusNodeNums(nodeDB->getNodeNum());
|
|
replayStatusIndex = 0;
|
|
LOG_INFO("Begin status replay: %u entries millis=%u", (unsigned)replayStatusOrder.size(), millis());
|
|
#endif
|
|
}
|
|
|
|
void PhoneAPI::prefetchReplayStatus()
|
|
{
|
|
#if MESHTASTIC_EXCLUDE_STATUSDB
|
|
return;
|
|
#else
|
|
bool added = false;
|
|
bool wasEmpty = false;
|
|
{
|
|
concurrency::LockGuard guard(&nodeInfoMutex);
|
|
wasEmpty = replayQueue.empty();
|
|
while (replayQueue.size() < kReplayPrefetchDepth && replayStatusIndex < replayStatusOrder.size()) {
|
|
NodeNum num = replayStatusOrder[replayStatusIndex++];
|
|
meshtastic_StatusMessage status;
|
|
if (!nodeDB->copyNodeStatus(num, status) || status.status[0] == '\0')
|
|
continue;
|
|
replayQueue.push_back(makeReplayStatusPacket(num, status));
|
|
added = true;
|
|
}
|
|
}
|
|
if (added && wasEmpty)
|
|
onNowHasData(0);
|
|
#endif
|
|
}
|
|
|
|
// Pop one cached satellite-DB packet from the active replay phase.
|
|
// Phases drain in order: positions -> telemetry -> environment -> status.
|
|
// When the current phase's cursor is exhausted (queue empty AND no more entries
|
|
// to snapshot), advance to the next phase. When all four phases are done,
|
|
// flip replayPhase back to IDLE and release the snapshot vectors.
|
|
//
|
|
// Returns true if a packet was placed in `out`; false if everything is drained.
|
|
bool PhoneAPI::popReplayPacket(meshtastic_MeshPacket &out)
|
|
{
|
|
while (replayPhase != REPLAY_PHASE_IDLE) {
|
|
// Prime the active phase: seed the snapshot vector on first entry,
|
|
// top up replayQueue from the snapshot up to kReplayPrefetchDepth.
|
|
switch (replayPhase) {
|
|
case REPLAY_PHASE_POSITIONS:
|
|
if (replayPositionOrder.empty() && replayPositionIndex == 0)
|
|
beginReplayPositions();
|
|
prefetchReplayPositions();
|
|
break;
|
|
case REPLAY_PHASE_TELEMETRY:
|
|
if (replayTelemetryOrder.empty() && replayTelemetryIndex == 0)
|
|
beginReplayTelemetry();
|
|
prefetchReplayTelemetry();
|
|
break;
|
|
case REPLAY_PHASE_ENVIRONMENT:
|
|
if (replayEnvironmentOrder.empty() && replayEnvironmentIndex == 0)
|
|
beginReplayEnvironment();
|
|
prefetchReplayEnvironment();
|
|
break;
|
|
case REPLAY_PHASE_STATUS:
|
|
if (replayStatusOrder.empty() && replayStatusIndex == 0)
|
|
beginReplayStatus();
|
|
prefetchReplayStatus();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
{
|
|
concurrency::LockGuard guard(&nodeInfoMutex);
|
|
if (!replayQueue.empty()) {
|
|
out = replayQueue.front();
|
|
replayQueue.pop_front();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Queue empty AND no more entries to feed it — phase is exhausted.
|
|
advanceReplayPhase();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void PhoneAPI::advanceReplayPhase()
|
|
{
|
|
switch (replayPhase) {
|
|
case REPLAY_PHASE_POSITIONS:
|
|
LOG_DEBUG("Replay drain: positions done (count=%u) millis=%u", (unsigned)replayPositionIndex, millis());
|
|
replayPhase = REPLAY_PHASE_TELEMETRY;
|
|
break;
|
|
case REPLAY_PHASE_TELEMETRY:
|
|
LOG_DEBUG("Replay drain: telemetry done (count=%u) millis=%u", (unsigned)replayTelemetryIndex, millis());
|
|
replayPhase = REPLAY_PHASE_ENVIRONMENT;
|
|
break;
|
|
case REPLAY_PHASE_ENVIRONMENT:
|
|
LOG_DEBUG("Replay drain: environment done (count=%u) millis=%u", (unsigned)replayEnvironmentIndex, millis());
|
|
replayPhase = REPLAY_PHASE_STATUS;
|
|
break;
|
|
case REPLAY_PHASE_STATUS:
|
|
LOG_INFO("Replay drain complete (status count=%u) millis=%u", (unsigned)replayStatusIndex, millis());
|
|
replayPositionOrder.clear();
|
|
replayPositionOrder.shrink_to_fit();
|
|
replayTelemetryOrder.clear();
|
|
replayTelemetryOrder.shrink_to_fit();
|
|
replayEnvironmentOrder.clear();
|
|
replayEnvironmentOrder.shrink_to_fit();
|
|
replayStatusOrder.clear();
|
|
replayStatusOrder.shrink_to_fit();
|
|
replayPositionIndex = 0;
|
|
replayTelemetryIndex = 0;
|
|
replayEnvironmentIndex = 0;
|
|
replayStatusIndex = 0;
|
|
replayPhase = REPLAY_PHASE_IDLE;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void PhoneAPI::releaseMqttClientProxyPhonePacket()
|
|
{
|
|
if (mqttClientProxyMessageForPhone) {
|
|
service->releaseMqttClientProxyMessageToPool(mqttClientProxyMessageForPhone);
|
|
mqttClientProxyMessageForPhone = NULL;
|
|
}
|
|
}
|
|
|
|
void PhoneAPI::releaseClientNotification()
|
|
{
|
|
if (clientNotification) {
|
|
service->releaseClientNotificationToPool(clientNotification);
|
|
clientNotification = NULL;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return true if we have data available to send to the phone
|
|
*/
|
|
bool PhoneAPI::available()
|
|
{
|
|
switch (state) {
|
|
case STATE_SEND_NOTHING:
|
|
return false;
|
|
case STATE_SEND_MY_INFO:
|
|
case STATE_SEND_UIDATA:
|
|
case STATE_SEND_CHANNELS:
|
|
case STATE_SEND_CONFIG:
|
|
case STATE_SEND_MODULECONFIG:
|
|
case STATE_SEND_METADATA:
|
|
case STATE_SEND_OWN_NODEINFO:
|
|
case STATE_SEND_FILEMANIFEST:
|
|
case STATE_SEND_COMPLETE_ID:
|
|
return true;
|
|
|
|
case STATE_SEND_OTHER_NODEINFOS: {
|
|
concurrency::LockGuard guard(&nodeInfoMutex);
|
|
if (nodeInfoQueue.empty()) {
|
|
// Drop the lock before prefetching; prefetchNodeInfos() will re-acquire it.
|
|
goto PREFETCH_NODEINFO;
|
|
}
|
|
}
|
|
return true; // Always say we have something, because we might need to advance our state machine
|
|
PREFETCH_NODEINFO:
|
|
prefetchNodeInfos();
|
|
return true;
|
|
case STATE_SEND_PACKETS: {
|
|
if (!queueStatusPacketForPhone)
|
|
queueStatusPacketForPhone = service->getQueueStatusForPhone();
|
|
if (!mqttClientProxyMessageForPhone)
|
|
mqttClientProxyMessageForPhone = service->getMqttClientProxyMessageForPhone();
|
|
if (!clientNotification)
|
|
clientNotification = service->getClientNotificationForPhone();
|
|
bool hasPacket = !!queueStatusPacketForPhone || !!mqttClientProxyMessageForPhone || !!clientNotification;
|
|
if (hasPacket)
|
|
return true;
|
|
|
|
#ifdef FSCom
|
|
if (xmodemPacketForPhone.control == meshtastic_XModem_Control_NUL)
|
|
xmodemPacketForPhone = xModem.getForPhone();
|
|
if (xmodemPacketForPhone.control != meshtastic_XModem_Control_NUL) {
|
|
xModem.resetForPhone();
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
#ifdef ARCH_ESP32
|
|
#if !MESHTASTIC_EXCLUDE_STOREFORWARD
|
|
// Check if StoreForward has packets stored for us.
|
|
if (!packetForPhone && storeForwardModule)
|
|
packetForPhone = storeForwardModule->getForPhone();
|
|
#endif
|
|
#endif
|
|
|
|
if (!packetForPhone)
|
|
packetForPhone = service->getForPhone();
|
|
hasPacket = !!packetForPhone;
|
|
if (hasPacket)
|
|
return true;
|
|
// Trailing replay drain — feeds cached satellite-DB packets alongside
|
|
// (lower priority than) live traffic.
|
|
return replayPending();
|
|
}
|
|
default:
|
|
LOG_ERROR("PhoneAPI::available unexpected state %d", state);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void PhoneAPI::sendNotification(meshtastic_LogRecord_Level level, uint32_t replyId, const char *message)
|
|
{
|
|
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
|
cn->has_reply_id = true;
|
|
cn->reply_id = replyId;
|
|
cn->level = meshtastic_LogRecord_Level_WARNING;
|
|
cn->time = getValidTime(RTCQualityFromNet);
|
|
strncpy(cn->message, message, sizeof(cn->message));
|
|
service->sendClientNotification(cn);
|
|
}
|
|
|
|
bool PhoneAPI::wasSeenRecently(uint32_t id)
|
|
{
|
|
for (int i = 0; i < 20; i++) {
|
|
if (recentToRadioPacketIds[i] == id) {
|
|
return true;
|
|
}
|
|
if (recentToRadioPacketIds[i] == 0) {
|
|
recentToRadioPacketIds[i] = id;
|
|
return false;
|
|
}
|
|
}
|
|
// If the array is full, shift all elements to the left and add the new id at the end
|
|
memmove(recentToRadioPacketIds, recentToRadioPacketIds + 1, (19) * sizeof(uint32_t));
|
|
recentToRadioPacketIds[19] = id;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Handle a packet that the phone wants us to send. It is our responsibility to free the packet to the pool
|
|
*/
|
|
bool PhoneAPI::handleToRadioPacket(meshtastic_MeshPacket &p)
|
|
{
|
|
printPacket("PACKET FROM PHONE", &p);
|
|
|
|
#if defined(ARCH_PORTDUINO)
|
|
// For use with the simulator, we should not ignore duplicate packets from the phone
|
|
if (SimRadio::instance == nullptr)
|
|
#endif
|
|
if (p.id > 0 && wasSeenRecently(p.id)) {
|
|
LOG_DEBUG("Ignore packet from phone, already seen recently");
|
|
return false;
|
|
}
|
|
|
|
if (p.decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP && lastPortNumToRadio[p.decoded.portnum] &&
|
|
Throttle::isWithinTimespanMs(lastPortNumToRadio[p.decoded.portnum], THIRTY_SECONDS_MS)) {
|
|
LOG_WARN("Rate limit portnum %d", p.decoded.portnum);
|
|
sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "TraceRoute can only be sent once every 30 seconds");
|
|
meshtastic_QueueStatus qs = router->getQueueStatus();
|
|
service->sendQueueStatusToPhone(qs, 0, p.id);
|
|
return false;
|
|
} else if (p.decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP && isBroadcast(p.to) && p.hop_limit > 0) {
|
|
sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "Multi-hop traceroute to broadcast address is not allowed");
|
|
meshtastic_QueueStatus qs = router->getQueueStatus();
|
|
service->sendQueueStatusToPhone(qs, 0, p.id);
|
|
return false;
|
|
} else if (IS_ONE_OF(p.decoded.portnum, meshtastic_PortNum_POSITION_APP, meshtastic_PortNum_WAYPOINT_APP,
|
|
meshtastic_PortNum_ALERT_APP, meshtastic_PortNum_TELEMETRY_APP) &&
|
|
lastPortNumToRadio[p.decoded.portnum] &&
|
|
Throttle::isWithinTimespanMs(lastPortNumToRadio[p.decoded.portnum], TEN_SECONDS_MS)) {
|
|
// TODO: [Issue #6700] Make this rate limit throttling scale up / down with the preset
|
|
LOG_WARN("Rate limit portnum %d", p.decoded.portnum);
|
|
meshtastic_QueueStatus qs = router->getQueueStatus();
|
|
service->sendQueueStatusToPhone(qs, 0, p.id);
|
|
// FIXME: Figure out why this continues to happen
|
|
// sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "Position can only be sent once every 5 seconds");
|
|
return false;
|
|
} else if (p.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP && lastPortNumToRadio[p.decoded.portnum] &&
|
|
Throttle::isWithinTimespanMs(lastPortNumToRadio[p.decoded.portnum], TWO_SECONDS_MS)) {
|
|
LOG_WARN("Rate limit portnum %d", p.decoded.portnum);
|
|
meshtastic_QueueStatus qs = router->getQueueStatus();
|
|
service->sendQueueStatusToPhone(qs, 0, p.id);
|
|
service->sendRoutingErrorResponse(meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED, &p);
|
|
// sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "Text messages can only be sent once every 2 seconds");
|
|
return false;
|
|
}
|
|
|
|
// Upgrade traceroute requests from phone to use reliable delivery, matching TraceRouteModule
|
|
if (p.decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP && !isBroadcast(p.to)) {
|
|
// Use reliable delivery for traceroute requests (which will be copied to traceroute responses by setReplyTo)
|
|
p.want_ack = true;
|
|
}
|
|
|
|
lastPortNumToRadio[p.decoded.portnum] = millis();
|
|
service->handleToRadio(p);
|
|
return true;
|
|
}
|
|
|
|
/// If the mesh service tells us fromNum has changed, tell the phone
|
|
int PhoneAPI::onNotify(uint32_t newValue)
|
|
{
|
|
bool timeout = checkConnectionTimeout(); // a handy place to check if we've heard from the phone (since the BLE version
|
|
// doesn't call this from idle)
|
|
|
|
if (state == STATE_SEND_PACKETS) {
|
|
LOG_INFO("Tell client we have new packets %u", newValue);
|
|
onNowHasData(newValue);
|
|
} else {
|
|
LOG_DEBUG("Client not yet interested in packets (state=%d)", state);
|
|
}
|
|
|
|
return timeout ? -1 : 0; // If we timed out, MeshService should stop iterating through observers as we just removed one
|
|
}
|