mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-14 15:31:31 +10:00
Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ccb2c918f | ||
|
|
6b49b7228a | ||
|
|
c194666885 | ||
|
|
d7cb5d7885 | ||
|
|
ffd144da83 | ||
|
|
1e8c9e7071 | ||
|
|
3c83e01d0e | ||
|
|
dc3947117e | ||
|
|
87d0850f95 | ||
|
|
5831952636 | ||
|
|
a6d61413c3 | ||
|
|
e393a5c410 | ||
|
|
8f2ecbdb4d | ||
|
|
6c28d11cee | ||
|
|
69f1b502cc | ||
|
|
3a498fbbe4 | ||
|
|
8f7dea0580 | ||
|
|
322f0262a8 | ||
|
|
00762393cf | ||
|
|
866c89f801 | ||
|
|
8c248927c8 | ||
|
|
188d895eb4 | ||
|
|
6f476f3475 | ||
|
|
50e1fe88e8 | ||
|
|
f9bedd8adc | ||
|
|
5a619c9031 | ||
|
|
8d3f9222ff | ||
|
|
f5335f22ea | ||
|
|
4a3f449555 | ||
|
|
608713470b | ||
|
|
f475be19c6 | ||
|
|
27cc76d5ed |
@@ -135,95 +135,6 @@ On top of authorization, any remote admin message that **mutates** state (not a
|
|||||||
- **Channel 0 PSK change** → every peer must re-learn the channel hash; cached NodeInfo becomes temporarily unreachable until the next broadcast.
|
- **Channel 0 PSK change** → every peer must re-learn the channel hash; cached NodeInfo becomes temporarily unreachable until the next broadcast.
|
||||||
- **`security.private_key` blanked via admin** → regenerates both halves (unless in Ham mode) and propagates the new public key via NodeInfo.
|
- **`security.private_key` blanked via admin** → regenerates both halves (unless in Ham mode) and propagates the new public key via NodeInfo.
|
||||||
|
|
||||||
## NodeDB Layout (v25)
|
|
||||||
|
|
||||||
`DEVICESTATE_CUR_VER = 25`, `DEVICESTATE_MIN_VER = 24`. The on-device NodeDB was split in v25 into a slim header table plus four optional satellite stores. Older v24 saves auto-migrate at boot. Old training-data instincts (`node->user.long_name`, `node->position.latitude_i`, `node->is_favorite`, `node->device_metrics.battery_level`) are wrong now — the fields aren't there. Read this section before touching anything that walks `nodeDB->meshNodes`.
|
|
||||||
|
|
||||||
### Slim `NodeInfoLite`
|
|
||||||
|
|
||||||
`UserLite` is flattened onto `NodeInfoLite` (no nested sub-message); `position` and `device_metrics` are removed entirely (tags reserved). MAC address is dropped. Long names are capped at 25 chars (`max_size:25` in `deviceonly.options`); `hw_model` and `role` are `int_size:8`. Encoded size dropped from ~166 B → ~105 B per node.
|
|
||||||
|
|
||||||
Booleans are bit-packed into `NodeInfoLite.bitfield`. **Do not read or write the bits directly** — use the inline helpers in `src/mesh/NodeDB.h`:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
nodeInfoLiteHasUser(n) // bit 5 — user fields populated
|
|
||||||
nodeInfoLiteIsFavorite(n) // bit 3
|
|
||||||
nodeInfoLiteIsIgnored(n) // bit 4
|
|
||||||
nodeInfoLiteIsMuted(n) // bit 1
|
|
||||||
nodeInfoLiteIsLicensed(n) // bit 6 — Ham mode peer
|
|
||||||
nodeInfoLiteIsKeyManuallyVerified(n) // bit 0
|
|
||||||
nodeInfoLiteHasIsUnmessagable(n) // bit 8 — "is_unmessagable was sent"
|
|
||||||
nodeInfoLiteIsUnmessagable(n) // bit 7
|
|
||||||
// via_mqtt is bit 2 (mask exposed; predicate uses the mask directly)
|
|
||||||
|
|
||||||
nodeInfoLiteSetBit(n, NODEINFO_BITFIELD_IS_FAVORITE_MASK, true); // setter
|
|
||||||
```
|
|
||||||
|
|
||||||
### Satellite stores
|
|
||||||
|
|
||||||
Four `std::unordered_map<NodeNum, …>` members on `NodeDB`, each gated by its own build flag:
|
|
||||||
|
|
||||||
| Map | Value type | Build flag |
|
|
||||||
| ----------------- | ------------------------------- | ---------------------------------- |
|
|
||||||
| `nodePositions` | `meshtastic_PositionLite` | `MESHTASTIC_EXCLUDE_POSITIONDB` |
|
|
||||||
| `nodeTelemetry` | `meshtastic_DeviceMetrics` | `MESHTASTIC_EXCLUDE_TELEMETRYDB` |
|
|
||||||
| `nodeEnvironment` | `meshtastic_EnvironmentMetrics` | `MESHTASTIC_EXCLUDE_ENVIRONMENTDB` |
|
|
||||||
| `nodeStatus` | `meshtastic_StatusMessage` | `MESHTASTIC_EXCLUDE_STATUSDB` |
|
|
||||||
|
|
||||||
Defaults are ON (i.e., maps **excluded**) for STM32WL only — see `src/mesh/mesh-pb-constants.h`. On every other arch all four maps are present. When excluded, the map member is absent and the corresponding accessors return `false`.
|
|
||||||
|
|
||||||
All four maps are guarded by **`mutable concurrency::Lock satelliteMutex`** — concurrent access from receive threads, the phone API state machine, and the renderer is the rule, not the exception.
|
|
||||||
|
|
||||||
### Accessor convention
|
|
||||||
|
|
||||||
**Never hand out pointers into the maps.** Use the copy-out accessors on `NodeDB`:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
bool copyNodePosition(NodeNum, meshtastic_PositionLite &out) const;
|
|
||||||
bool copyNodeTelemetry(NodeNum, meshtastic_DeviceMetrics &out) const;
|
|
||||||
bool copyNodeEnvironment(NodeNum, meshtastic_EnvironmentMetrics &out) const;
|
|
||||||
bool copyNodeStatus(NodeNum, meshtastic_StatusMessage &out) const;
|
|
||||||
```
|
|
||||||
|
|
||||||
Each takes the lock, copies the value if present, returns `false` if the entry is absent or the DB is excluded. Pass-by-out-param is deliberate — pointer-style accessors would invite UAF and lock-leak bugs across the renderer. The "has any X" convenience predicates (`hasValidPosition` etc.) are implemented in terms of these.
|
|
||||||
|
|
||||||
Writers go through `setNodeStatus`, `updatePosition`, `updateTelemetry` (which dispatches on `which_variant` for device vs environment metrics) — these own the lock and the eviction hooks.
|
|
||||||
|
|
||||||
### Eviction
|
|
||||||
|
|
||||||
Every code path that drops a node from the header table must also evict the satellites. The single chokepoint is `eraseNodeSatellites(NodeNum)`; it's already called from `getOrCreateMeshNode`'s oldest-boring eviction, `removeNodeByNum`, both branches of `resetNodes`, `cleanupMeshDB`, `addFromContact`'s ignored-branch, and `AdminModule`'s `set_ignored_node`. Add new eviction sites here, not by calling `.erase()` directly.
|
|
||||||
|
|
||||||
### Gradient sync (opt-in via special nonces)
|
|
||||||
|
|
||||||
`client_capabilities` is **not** a thing in this branch. Phone clients opt into the new sync flow by sending one of two values in the `ToRadio.want_config_id`:
|
|
||||||
|
|
||||||
- `SPECIAL_NONCE_GRADIENT_SYNC` (69422) — full config + thin NodeInfo + replay phases.
|
|
||||||
- `SPECIAL_NONCE_GRADIENT_ONLY_NODES` (69423) — skip config segments, NodeInfo + replay only.
|
|
||||||
|
|
||||||
`PhoneAPI::clientWantsGradientSync()` is the single switch. When true, `STATE_SEND_OTHER_NODEINFOS` is followed by:
|
|
||||||
|
|
||||||
```text
|
|
||||||
STATE_REPLAY_POSITIONS → STATE_REPLAY_TELEMETRY → STATE_REPLAY_ENVIRONMENT → STATE_REPLAY_STATUS
|
|
||||||
```
|
|
||||||
|
|
||||||
Each replay phase walks the corresponding satellite map and emits synthetic `MeshPacket`s on the matching portnum (`POSITION_APP`, `TELEMETRY_APP` for both device + environment variants, `STATUS_MESSAGE_APP`). Legacy clients (no special nonce) get the bundled-NodeInfo path with position/device_metrics joined back in by `ConvertToNodeInfo(lite, pos*, dm*)` — wire bytes are byte-identical to pre-v25 for them.
|
|
||||||
|
|
||||||
`ConvertToNodeInfoThin(lite)` is the gradient-sync emitter (no position/telemetry).
|
|
||||||
|
|
||||||
### v24 → v25 migration
|
|
||||||
|
|
||||||
The legacy migration code lives in **`src/mesh/NodeDBLegacyMigration.cpp`**, not in `NodeDB.cpp`. It owns the `meshtastic_NodeDatabase_Legacy` callback and `NodeDB::migrateLegacyNodeDatabase()`. The legacy proto descriptor is `protobufs/meshtastic/deviceonly_legacy.proto` (only included by the migration TU). The boot path peeks the file's leading version tag, runs the migration if `version < 25`, then re-saves in v25 layout. The legacy descriptor is scheduled for removal once `DEVICESTATE_MIN_VER` is bumped.
|
|
||||||
|
|
||||||
### Read-site rules of thumb
|
|
||||||
|
|
||||||
- Never `node->position.X` / `node->device_metrics.X` — those fields no longer exist. Pull from the satellite map via `copyNodePosition` / `copyNodeTelemetry`.
|
|
||||||
- Never `node->user.long_name` — `long_name`, `short_name`, `public_key`, `hw_model`, `role`, `macaddr` (gone), `is_licensed`, `is_unmessagable` are flat on `NodeInfoLite`.
|
|
||||||
- Never `node->is_favorite` / `node->is_ignored` / `node->via_mqtt` / `node->is_key_manually_verified` — use the bitfield helpers.
|
|
||||||
- Never assume `nodeDB->getMeshNode(num)->position.time` — call `copyNodePosition` and check the return.
|
|
||||||
- Don't lock `satelliteMutex` yourself in renderer code; the copy-out accessors already do.
|
|
||||||
|
|
||||||
Unit tests for the conversion layer live in `test/test_type_conversions/test_main.cpp` (Unity) — bitfield round-trips, `long_name` truncation, thin-vs-full conversions. Add cases there when extending the schema.
|
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -664,8 +575,6 @@ Grouped by purpose. Full argument shapes in `mcp-server/README.md`; a few high-v
|
|||||||
|
|
||||||
`confirm=True` is a tool-level gate on top of whatever permission prompt your MCP host shows. **Don't bypass it** by asking the host to auto-approve — it exists specifically because MCP hosts sometimes remember "always allow this tool" and that's dangerous for `factory_reset`, `erase_and_flash`, `uhubctl_power(action='off')`, and `uhubctl_cycle`.
|
`confirm=True` is a tool-level gate on top of whatever permission prompt your MCP host shows. **Don't bypass it** by asking the host to auto-approve — it exists specifically because MCP hosts sometimes remember "always allow this tool" and that's dangerous for `factory_reset`, `erase_and_flash`, `uhubctl_power(action='off')`, and `uhubctl_cycle`.
|
||||||
|
|
||||||
**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 `mcp-server/README.md` § "TCP / native-host nodes".
|
|
||||||
|
|
||||||
### Hardware test suite (`mcp-server/run-tests.sh`)
|
### Hardware test suite (`mcp-server/run-tests.sh`)
|
||||||
|
|
||||||
The wrapper auto-detects connected devices (VID → role map: `0x239A` → `nrf52`, `0x303A`/`0x10C4` → `esp32s3`), maps each role to a PlatformIO env (`nrf52` → `rak4631`, `esp32s3` → `heltec-v3`, overridable via `MESHTASTIC_MCP_ENV_<ROLE>`), then invokes pytest. Zero pre-flight config needed from the operator.
|
The wrapper auto-detects connected devices (VID → role map: `0x239A` → `nrf52`, `0x303A`/`0x10C4` → `esp32s3`), maps each role to a PlatformIO env (`nrf52` → `rak4631`, `esp32s3` → `heltec-v3`, overridable via `MESHTASTIC_MCP_ENV_<ROLE>`), then invokes pytest. Zero pre-flight config needed from the operator.
|
||||||
|
|||||||
@@ -32,15 +32,10 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
working-directory: meshtasticd
|
working-directory: meshtasticd
|
||||||
run: |
|
run: |
|
||||||
# Build-tools (notably platformio) come from the Meshtastic project
|
|
||||||
# on the OpenSUSE Build Service:
|
|
||||||
# https://build.opensuse.org/project/show/network:Meshtastic:build-tools
|
|
||||||
echo 'deb http://download.opensuse.org/repositories/network:/Meshtastic:/build-tools/xUbuntu_24.04/ /' \
|
|
||||||
| sudo tee /etc/apt/sources.list.d/network:Meshtastic:build-tools.list
|
|
||||||
curl -fsSL https://download.opensuse.org/repositories/network:Meshtastic:build-tools/xUbuntu_24.04/Release.key \
|
|
||||||
| gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/network_Meshtastic_build-tools.gpg >/dev/null
|
|
||||||
sudo apt-get update -y --fix-missing
|
sudo apt-get update -y --fix-missing
|
||||||
sudo apt-get install -y build-essential devscripts equivs
|
sudo apt-get install -y software-properties-common build-essential devscripts equivs
|
||||||
|
sudo add-apt-repository ppa:meshtastic/build-tools -y
|
||||||
|
sudo apt-get update -y --fix-missing
|
||||||
sudo mk-build-deps --install --remove --tool='apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends --yes' debian/control
|
sudo mk-build-deps --install --remove --tool='apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends --yes' debian/control
|
||||||
|
|
||||||
- name: Import GPG key
|
- name: Import GPG key
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
brew update
|
brew update
|
||||||
brew install platformio yaml-cpp libuv openssl@3 libusb argp-standalone pkg-config ulfius
|
brew install platformio yaml-cpp libuv openssl@3 libusb argp-standalone pkg-config
|
||||||
|
|
||||||
- name: Get release version string
|
- name: Get release version string
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ plugins:
|
|||||||
uri: https://github.com/trunk-io/plugins
|
uri: https://github.com/trunk-io/plugins
|
||||||
lint:
|
lint:
|
||||||
enabled:
|
enabled:
|
||||||
- checkov@3.2.526
|
- checkov@3.2.525
|
||||||
- renovate@43.150.0
|
- renovate@43.150.0
|
||||||
- prettier@3.8.3
|
- prettier@3.8.3
|
||||||
- trufflehog@3.95.2
|
- trufflehog@3.95.2
|
||||||
|
|||||||
@@ -126,17 +126,16 @@ Sequence these; don't parallelize on the same port.
|
|||||||
|
|
||||||
## Environment variables (test harness)
|
## Environment variables (test harness)
|
||||||
|
|
||||||
| Var | Purpose |
|
| Var | Purpose |
|
||||||
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `MESHTASTIC_MCP_ENV_<ROLE>` | Override PlatformIO env for a role (e.g. `MESHTASTIC_MCP_ENV_NRF52=rak4631-dap`). Default map: `nrf52→rak4631`, `esp32s3→heltec-v3`. |
|
| `MESHTASTIC_MCP_ENV_<ROLE>` | Override PlatformIO env for a role (e.g. `MESHTASTIC_MCP_ENV_NRF52=rak4631-dap`). Default map: `nrf52→rak4631`, `esp32s3→heltec-v3`. |
|
||||||
| `MESHTASTIC_MCP_SEED` | PSK seed for the session test profile. Defaults to `mcp-<user>-<host>`. |
|
| `MESHTASTIC_MCP_SEED` | PSK seed for the session test profile. Defaults to `mcp-<user>-<host>`. |
|
||||||
| `MESHTASTIC_MCP_FLASH_LOG` | File path to tee pio/esptool/nrfutil/picotool output. `run-tests.sh` sets this to `tests/flash.log` so the TUI can stream live flash progress. |
|
| `MESHTASTIC_MCP_FLASH_LOG` | File path to tee pio/esptool/nrfutil/picotool output. `run-tests.sh` sets this to `tests/flash.log` so the TUI can stream live flash progress. |
|
||||||
| `MESHTASTIC_MCP_TCP_HOST` | `host` or `host:port` of a `meshtasticd` daemon (e.g. the `native-macos` build). Surfaces it in `list_devices` as `tcp://host:port` so `connect()`-based tools target it transparently. Default port 4403. |
|
| `MESHTASTIC_UHUBCTL_BIN` | Absolute path to `uhubctl` binary. Default: PATH lookup. |
|
||||||
| `MESHTASTIC_UHUBCTL_BIN` | Absolute path to `uhubctl` binary. Default: PATH lookup. |
|
| `MESHTASTIC_UHUBCTL_LOCATION_<ROLE>` | Pin a role to a specific uhubctl hub location (e.g. `1-1.3`). Wins over VID auto-detection — use when multiple devices share a VID. |
|
||||||
| `MESHTASTIC_UHUBCTL_LOCATION_<ROLE>` | Pin a role to a specific uhubctl hub location (e.g. `1-1.3`). Wins over VID auto-detection — use when multiple devices share a VID. |
|
| `MESHTASTIC_UHUBCTL_PORT_<ROLE>` | Pin a role to a specific hub port number. Required alongside `LOCATION_<ROLE>`. |
|
||||||
| `MESHTASTIC_UHUBCTL_PORT_<ROLE>` | Pin a role to a specific hub port number. Required alongside `LOCATION_<ROLE>`. |
|
| `MESHTASTIC_UI_CAMERA_BACKEND` | Camera backend for UI tier + `capture_screen` tool: `opencv` / `ffmpeg` / `null` / `auto` (default). |
|
||||||
| `MESHTASTIC_UI_CAMERA_BACKEND` | Camera backend for UI tier + `capture_screen` tool: `opencv` / `ffmpeg` / `null` / `auto` (default). |
|
| `MESHTASTIC_UI_CAMERA_DEVICE` | Generic camera device (index or path). Used by the UI tier when no per-role var is set. |
|
||||||
| `MESHTASTIC_UI_CAMERA_DEVICE` | Generic camera device (index or path). Used by the UI tier when no per-role var is set. |
|
| `MESHTASTIC_UI_CAMERA_DEVICE_<ROLE>` | Per-role camera pinning (e.g. `MESHTASTIC_UI_CAMERA_DEVICE_ESP32S3=0` for the OLED-bearing heltec-v3). |
|
||||||
| `MESHTASTIC_UI_CAMERA_DEVICE_<ROLE>` | Per-role camera pinning (e.g. `MESHTASTIC_UI_CAMERA_DEVICE_ESP32S3=0` for the OLED-bearing heltec-v3). |
|
| `MESHTASTIC_UI_OCR_BACKEND` | OCR engine selection: `easyocr` / `pytesseract` / `null` / `auto` (default). |
|
||||||
| `MESHTASTIC_UI_OCR_BACKEND` | OCR engine selection: `easyocr` / `pytesseract` / `null` / `auto` (default). |
|
| `MESHTASTIC_UI_TUI_CAMERA` | Set to `1` to mount the live camera-feed panel in `meshtastic-mcp-test-tui`. |
|
||||||
| `MESHTASTIC_UI_TUI_CAMERA` | Set to `1` to mount the live camera-feed panel in `meshtastic-mcp-test-tui`. |
|
|
||||||
|
|||||||
+1
-3
@@ -3,17 +3,15 @@
|
|||||||
# trunk-ignore-all(hadolint/DL3008): Do not pin apt package versions
|
# trunk-ignore-all(hadolint/DL3008): Do not pin apt package versions
|
||||||
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
|
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
|
||||||
|
|
||||||
FROM debian:trixie AS builder
|
FROM python:3.14-slim-trixie AS builder
|
||||||
ARG PIO_ENV=native
|
ARG PIO_ENV=native
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
ENV TZ=Etc/UTC
|
ENV TZ=Etc/UTC
|
||||||
|
|
||||||
# Install Dependencies
|
# Install Dependencies
|
||||||
ENV PIP_ROOT_USER_ACTION=ignore
|
ENV PIP_ROOT_USER_ACTION=ignore
|
||||||
ENV PIP_BREAK_SYSTEM_PACKAGES=1
|
|
||||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||||
curl wget g++ zip git ca-certificates pkg-config \
|
curl wget g++ zip git ca-certificates pkg-config \
|
||||||
python3-pip python3-grpc-tools \
|
|
||||||
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
|
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
|
||||||
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
|
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
|
||||||
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \
|
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \
|
||||||
|
|||||||
+2
-8
@@ -4,18 +4,12 @@
|
|||||||
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
|
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
|
||||||
|
|
||||||
# Ensure the Alpine version is updated in both stages of the container!
|
# Ensure the Alpine version is updated in both stages of the container!
|
||||||
FROM alpine:3.23 AS builder
|
FROM python:3.14-alpine3.23 AS builder
|
||||||
ARG PIO_ENV=native
|
ARG PIO_ENV=native
|
||||||
|
|
||||||
# Enable Alpine community repository (for 'py3-grpcio-tools')
|
|
||||||
RUN echo "https://dl-cdn.alpinelinux.org/alpine/v$(cut -d. -f1,2 /etc/alpine-release)/community" >> /etc/apk/repositories
|
|
||||||
|
|
||||||
# Install Dependencies
|
|
||||||
ENV PIP_ROOT_USER_ACTION=ignore
|
ENV PIP_ROOT_USER_ACTION=ignore
|
||||||
ENV PIP_BREAK_SYSTEM_PACKAGES=1
|
|
||||||
RUN apk --no-cache add \
|
RUN apk --no-cache add \
|
||||||
bash g++ libstdc++-dev linux-headers zip git ca-certificates libbsd-dev \
|
bash g++ libstdc++-dev linux-headers zip git ca-certificates libbsd-dev \
|
||||||
py3-pip py3-grpcio-tools \
|
|
||||||
libgpiod-dev yaml-cpp-dev bluez-dev \
|
libgpiod-dev yaml-cpp-dev bluez-dev \
|
||||||
libusb-dev i2c-tools-dev libuv-dev openssl-dev pkgconf argp-standalone \
|
libusb-dev i2c-tools-dev libuv-dev openssl-dev pkgconf argp-standalone \
|
||||||
libx11-dev libinput-dev libxkbcommon-dev sqlite-dev sdl2-dev \
|
libx11-dev libinput-dev libxkbcommon-dev sqlite-dev sdl2-dev \
|
||||||
|
|||||||
@@ -0,0 +1,941 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import queue
|
||||||
|
import random
|
||||||
|
import shutil
|
||||||
|
import socket
|
||||||
|
import select
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import termios
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import tty
|
||||||
|
from collections import deque
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, TextIO
|
||||||
|
|
||||||
|
|
||||||
|
START1 = 0x94
|
||||||
|
START2 = 0xC3
|
||||||
|
HEADER_LEN = 4
|
||||||
|
DEFAULT_API_PORT = 4403
|
||||||
|
DEFAULT_HOP_LIMIT = 0
|
||||||
|
LOCAL_ESCAPE_BYTE = b"\x1d" # Ctrl+]
|
||||||
|
MISSING_SEQ_RETRY_INTERVAL_SEC = 1.0
|
||||||
|
INPUT_BATCH_WINDOW_SEC = .5
|
||||||
|
INPUT_BATCH_MAX_BYTES = 64
|
||||||
|
HEARTBEAT_IDLE_DELAY_SEC = 5.0
|
||||||
|
HEARTBEAT_REPEAT_SEC = 15.0
|
||||||
|
HEARTBEAT_POLL_INTERVAL_SEC = 0.25
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Tiny DMShell client for Meshtastic native TCP API",
|
||||||
|
epilog=(
|
||||||
|
"Examples:\n"
|
||||||
|
" bin/dmshell_client.py --to !170896f7\n"
|
||||||
|
" bin/dmshell_client.py --to 0x170896f7 --command 'uname -a' --command 'id'"
|
||||||
|
),
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
parser.add_argument("--host", default="127.0.0.1", help="meshtasticd API host")
|
||||||
|
parser.add_argument("--port", type=int, default=DEFAULT_API_PORT, help="meshtasticd API port")
|
||||||
|
parser.add_argument(
|
||||||
|
"--serial",
|
||||||
|
nargs="?",
|
||||||
|
const="auto",
|
||||||
|
default=None,
|
||||||
|
help="use USB serial transport (optionally provide device path, default: auto-detect)",
|
||||||
|
)
|
||||||
|
parser.add_argument("--baud", type=int, default=115200, help="serial baud rate when using --serial")
|
||||||
|
parser.add_argument("--to", required=True, help="destination node number, e.g. !170896f7 or 0x170896f7")
|
||||||
|
parser.add_argument("--channel", type=int, default=0, help="channel index to use")
|
||||||
|
parser.add_argument("--cols", type=int, default=None, help="initial terminal columns (default: detect local terminal)")
|
||||||
|
parser.add_argument("--rows", type=int, default=None, help="initial terminal rows (default: detect local terminal)")
|
||||||
|
parser.add_argument("--command", action="append", default=[], help="send a command line after opening")
|
||||||
|
parser.add_argument("--close-after", type=float, default=2.0, help="seconds to wait before closing in command mode")
|
||||||
|
parser.add_argument("--timeout", type=float, default=10.0, help="seconds to wait for API/session events")
|
||||||
|
parser.add_argument("--verbose", action="store_true", help="print extra protocol events")
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def repo_root() -> Path:
|
||||||
|
return Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
def load_proto_modules() -> object:
|
||||||
|
try:
|
||||||
|
import google.protobuf # noqa: F401
|
||||||
|
except ImportError as exc:
|
||||||
|
raise SystemExit("python package 'protobuf' is required to run this client") from exc
|
||||||
|
|
||||||
|
protoc = shutil.which("protoc")
|
||||||
|
if not protoc:
|
||||||
|
raise SystemExit("'protoc' is required to generate temporary Python protobuf bindings")
|
||||||
|
|
||||||
|
out_dir = Path(tempfile.mkdtemp(prefix="meshtastic_dmshell_proto_"))
|
||||||
|
proto_dir = repo_root() / "protobufs"
|
||||||
|
|
||||||
|
# Compile all required protos for DMShell client (mesh and dependencies)
|
||||||
|
# Excludes nanopb.proto and other complex build artifacts
|
||||||
|
required_protos = [
|
||||||
|
"mesh.proto",
|
||||||
|
"channel.proto",
|
||||||
|
"config.proto",
|
||||||
|
"device_ui.proto",
|
||||||
|
"module_config.proto",
|
||||||
|
"atak.proto",
|
||||||
|
"portnums.proto",
|
||||||
|
"telemetry.proto",
|
||||||
|
"xmodem.proto",
|
||||||
|
]
|
||||||
|
proto_files = [proto_dir / "meshtastic" / name for name in required_protos]
|
||||||
|
for pf in proto_files:
|
||||||
|
if not pf.exists():
|
||||||
|
raise SystemExit(f"could not find required proto file: {pf}")
|
||||||
|
|
||||||
|
# Create __init__.py to make meshtastic a package
|
||||||
|
(out_dir / "meshtastic").mkdir(exist_ok=True)
|
||||||
|
(out_dir / "meshtastic" / "__init__.py").touch()
|
||||||
|
|
||||||
|
# Build protoc command with just the meshtastic proto directory as include path
|
||||||
|
# protoc will use its built-in includes for standard google protobuf types
|
||||||
|
cmd = [protoc, f"-I{proto_dir}", f"--python_out={out_dir}", *[str(path) for path in proto_files]]
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"protoc stderr: {result.stderr}", file=sys.stderr)
|
||||||
|
print(f"protoc stdout: {result.stdout}", file=sys.stderr)
|
||||||
|
print(f"protoc command: {' '.join(cmd)}", file=sys.stderr)
|
||||||
|
raise SystemExit(f"protoc failed with return code {result.returncode}")
|
||||||
|
|
||||||
|
# Create _pb2_grpc module stub if not present (protoc 3.20+)
|
||||||
|
mesh_pb2_file = out_dir / "meshtastic" / "mesh_pb2.py"
|
||||||
|
if not mesh_pb2_file.exists():
|
||||||
|
raise SystemExit(f"protoc did not generate mesh_pb2.py in {out_dir / 'meshtastic'}")
|
||||||
|
|
||||||
|
sys.path.insert(0, str(out_dir))
|
||||||
|
try:
|
||||||
|
from meshtastic import mesh_pb2, portnums_pb2 # type: ignore
|
||||||
|
except ImportError as exc:
|
||||||
|
print(f"Failed to import protobuf modules. Output dir contents:", file=sys.stderr)
|
||||||
|
for item in (out_dir / "meshtastic").iterdir():
|
||||||
|
print(f" {item.name}", file=sys.stderr)
|
||||||
|
raise SystemExit(f"could not import meshtastic proto modules: {exc}") from exc
|
||||||
|
|
||||||
|
# Return an object that has both modules accessible
|
||||||
|
class ProtoModules:
|
||||||
|
pass
|
||||||
|
|
||||||
|
pb2 = ProtoModules()
|
||||||
|
pb2.mesh = mesh_pb2
|
||||||
|
pb2.portnums = portnums_pb2
|
||||||
|
return pb2
|
||||||
|
|
||||||
|
|
||||||
|
def parse_node_num(raw: str) -> int:
|
||||||
|
value = raw.strip()
|
||||||
|
if value.startswith("!"):
|
||||||
|
value = value[1:]
|
||||||
|
if value.lower().startswith("0x"):
|
||||||
|
return int(value, 16)
|
||||||
|
if any(ch in "abcdefABCDEF" for ch in value):
|
||||||
|
return int(value, 16)
|
||||||
|
return int(value, 10)
|
||||||
|
|
||||||
|
|
||||||
|
class SerialTransport:
|
||||||
|
def __init__(self, serial_obj):
|
||||||
|
self._serial = serial_obj
|
||||||
|
|
||||||
|
def recv(self, length: int) -> bytes:
|
||||||
|
return self._serial.read(length)
|
||||||
|
|
||||||
|
def sendall(self, data: bytes) -> None:
|
||||||
|
self._serial.write(data)
|
||||||
|
self._serial.flush()
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
self._serial.close()
|
||||||
|
|
||||||
|
|
||||||
|
def detect_meshtastic_serial_port() -> str:
|
||||||
|
try:
|
||||||
|
from serial.tools import list_ports
|
||||||
|
except ImportError as exc:
|
||||||
|
raise SystemExit("python package 'pyserial' is required for --serial mode") from exc
|
||||||
|
|
||||||
|
ports = list(list_ports.comports())
|
||||||
|
if not ports:
|
||||||
|
raise SystemExit("no serial ports found for --serial mode")
|
||||||
|
|
||||||
|
scored: list[tuple[int, str]] = []
|
||||||
|
for port in ports:
|
||||||
|
text = " ".join(
|
||||||
|
filter(
|
||||||
|
None,
|
||||||
|
[port.device, port.description, port.manufacturer, port.product, port.hwid],
|
||||||
|
)
|
||||||
|
).lower()
|
||||||
|
score = 0
|
||||||
|
if "meshtastic" in text:
|
||||||
|
score += 100
|
||||||
|
if "lora" in text or "mesh" in text:
|
||||||
|
score += 10
|
||||||
|
if "ttyacm" in (port.device or "").lower() or "ttyusb" in (port.device or "").lower():
|
||||||
|
score += 1
|
||||||
|
scored.append((score, port.device))
|
||||||
|
|
||||||
|
scored.sort(reverse=True)
|
||||||
|
best_score, best_device = scored[0]
|
||||||
|
if best_score <= 0 and len(scored) > 1:
|
||||||
|
raise SystemExit(
|
||||||
|
"could not confidently auto-detect a Meshtastic serial port; pass --serial /dev/ttyXXX explicitly"
|
||||||
|
)
|
||||||
|
return best_device
|
||||||
|
|
||||||
|
|
||||||
|
def open_transport(args: argparse.Namespace):
|
||||||
|
if args.serial is None:
|
||||||
|
sock = socket.create_connection((args.host, args.port), timeout=args.timeout)
|
||||||
|
sock.settimeout(None)
|
||||||
|
return sock
|
||||||
|
|
||||||
|
serial_path = args.serial
|
||||||
|
if serial_path == "auto":
|
||||||
|
serial_path = detect_meshtastic_serial_port()
|
||||||
|
print(f"[dmshell] using serial port {serial_path}", file=sys.stderr)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import serial
|
||||||
|
except ImportError as exc:
|
||||||
|
raise SystemExit("python package 'pyserial' is required for --serial mode") from exc
|
||||||
|
|
||||||
|
try:
|
||||||
|
serial_obj = serial.Serial(serial_path, baudrate=args.baud, timeout=None, write_timeout=2)
|
||||||
|
except Exception as exc:
|
||||||
|
raise SystemExit(f"failed to open serial device {serial_path}: {exc}") from exc
|
||||||
|
|
||||||
|
return SerialTransport(serial_obj)
|
||||||
|
|
||||||
|
|
||||||
|
def recv_exact(transport, length: int) -> bytes:
|
||||||
|
chunks = bytearray()
|
||||||
|
while len(chunks) < length:
|
||||||
|
piece = transport.recv(length - len(chunks))
|
||||||
|
if not piece:
|
||||||
|
raise ConnectionError("connection closed by transport")
|
||||||
|
chunks.extend(piece)
|
||||||
|
return bytes(chunks)
|
||||||
|
|
||||||
|
|
||||||
|
def detect_local_terminal_size() -> tuple[int, int]:
|
||||||
|
size = shutil.get_terminal_size(fallback=(100, 40))
|
||||||
|
cols = max(1, int(size.columns))
|
||||||
|
rows = max(1, int(size.lines))
|
||||||
|
return cols, rows
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_initial_terminal_size(cols_override: Optional[int], rows_override: Optional[int]) -> tuple[int, int]:
|
||||||
|
detected_cols, detected_rows = detect_local_terminal_size()
|
||||||
|
cols = detected_cols if cols_override is None else max(1, cols_override)
|
||||||
|
rows = detected_rows if rows_override is None else max(1, rows_override)
|
||||||
|
return cols, rows
|
||||||
|
|
||||||
|
|
||||||
|
def recv_stream_frame(transport) -> bytes:
|
||||||
|
while True:
|
||||||
|
start = recv_exact(transport, 1)[0]
|
||||||
|
if start != START1:
|
||||||
|
continue
|
||||||
|
if recv_exact(transport, 1)[0] != START2:
|
||||||
|
continue
|
||||||
|
header = recv_exact(transport, 2)
|
||||||
|
length = (header[0] << 8) | header[1]
|
||||||
|
return recv_exact(transport, length)
|
||||||
|
|
||||||
|
|
||||||
|
def send_stream_frame(transport, payload: bytes) -> None:
|
||||||
|
if len(payload) > 0xFFFF:
|
||||||
|
raise ValueError("payload too large for stream API")
|
||||||
|
header = bytes((START1, START2, (len(payload) >> 8) & 0xFF, len(payload) & 0xFF))
|
||||||
|
transport.sendall(header + payload)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SentShellFrame:
|
||||||
|
op: int
|
||||||
|
session_id: int
|
||||||
|
seq: int
|
||||||
|
ack_seq: int
|
||||||
|
payload: bytes = b""
|
||||||
|
cols: int = 0
|
||||||
|
rows: int = 0
|
||||||
|
flags: int = 0
|
||||||
|
last_tx_seq: int = 0
|
||||||
|
last_rx_seq: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SessionState:
|
||||||
|
pb2: object # ProtoModules with mesh and portnums attributes
|
||||||
|
target: int
|
||||||
|
channel: int
|
||||||
|
verbose: bool
|
||||||
|
session_id: int = field(default_factory=lambda: random.randint(1, 0x7FFFFFFF))
|
||||||
|
next_seq: int = 1
|
||||||
|
last_rx_seq: int = 0
|
||||||
|
next_expected_rx_seq: int = 1
|
||||||
|
highest_seen_rx_seq: int = 0
|
||||||
|
active: bool = False
|
||||||
|
stopped: bool = False
|
||||||
|
opened_event: threading.Event = field(default_factory=threading.Event)
|
||||||
|
closed_event: threading.Event = field(default_factory=threading.Event)
|
||||||
|
event_queue: "queue.Queue[str]" = field(default_factory=queue.Queue)
|
||||||
|
tx_lock: threading.Lock = field(default_factory=threading.Lock)
|
||||||
|
socket_lock: threading.Lock = field(default_factory=threading.Lock)
|
||||||
|
tx_history: deque[SentShellFrame] = field(default_factory=lambda: deque(maxlen=50))
|
||||||
|
pending_rx_frames: dict[int, object] = field(default_factory=dict)
|
||||||
|
last_requested_missing_seq: int = 0
|
||||||
|
last_missing_request_time: float = 0.0
|
||||||
|
requested_missing_seqs: set[int] = field(default_factory=set)
|
||||||
|
replay_log_lock: threading.Lock = field(default_factory=threading.Lock)
|
||||||
|
replay_log_file: Optional[TextIO] = None
|
||||||
|
replay_log_path: Optional[Path] = None
|
||||||
|
last_transport_activity_time: float = field(default_factory=time.monotonic)
|
||||||
|
last_heartbeat_sent_time: float = 0.0
|
||||||
|
|
||||||
|
def alloc_seq(self) -> int:
|
||||||
|
with self.tx_lock:
|
||||||
|
value = self.next_seq
|
||||||
|
self.next_seq += 1
|
||||||
|
return value
|
||||||
|
|
||||||
|
def current_ack_seq(self) -> int:
|
||||||
|
with self.tx_lock:
|
||||||
|
return self.last_rx_seq
|
||||||
|
|
||||||
|
def highest_sent_seq(self) -> int:
|
||||||
|
with self.tx_lock:
|
||||||
|
return max(0, self.next_seq - 1)
|
||||||
|
|
||||||
|
def note_outbound_packet(self, heartbeat: bool = False) -> None:
|
||||||
|
with self.tx_lock:
|
||||||
|
now = time.monotonic()
|
||||||
|
if heartbeat:
|
||||||
|
self.last_heartbeat_sent_time = now
|
||||||
|
else:
|
||||||
|
self.last_transport_activity_time = now
|
||||||
|
|
||||||
|
def note_inbound_packet(self) -> None:
|
||||||
|
with self.tx_lock:
|
||||||
|
self.last_transport_activity_time = time.monotonic()
|
||||||
|
|
||||||
|
def heartbeat_due(self) -> bool:
|
||||||
|
with self.tx_lock:
|
||||||
|
now = time.monotonic()
|
||||||
|
if (now - self.last_transport_activity_time) < HEARTBEAT_IDLE_DELAY_SEC:
|
||||||
|
return False
|
||||||
|
if self.last_heartbeat_sent_time <= self.last_transport_activity_time:
|
||||||
|
return True
|
||||||
|
return (now - self.last_heartbeat_sent_time) >= HEARTBEAT_REPEAT_SEC
|
||||||
|
|
||||||
|
def note_peer_reported_tx_seq(self, seq: int) -> None:
|
||||||
|
with self.tx_lock:
|
||||||
|
if seq > self.highest_seen_rx_seq:
|
||||||
|
self.highest_seen_rx_seq = seq
|
||||||
|
|
||||||
|
def note_received_seq(self, seq: int) -> tuple[str, Optional[int]]:
|
||||||
|
with self.tx_lock:
|
||||||
|
if seq == 0:
|
||||||
|
return ("process", None)
|
||||||
|
if seq < self.next_expected_rx_seq:
|
||||||
|
if self.highest_seen_rx_seq >= self.next_expected_rx_seq:
|
||||||
|
return ("gap", self.next_expected_rx_seq)
|
||||||
|
return ("duplicate", None)
|
||||||
|
if seq > self.next_expected_rx_seq:
|
||||||
|
if seq > self.highest_seen_rx_seq:
|
||||||
|
self.highest_seen_rx_seq = seq
|
||||||
|
return ("gap", self.next_expected_rx_seq)
|
||||||
|
self.last_rx_seq = seq
|
||||||
|
self.next_expected_rx_seq = seq + 1
|
||||||
|
if self.last_requested_missing_seq != 0 and self.next_expected_rx_seq > self.last_requested_missing_seq:
|
||||||
|
self.last_requested_missing_seq = 0
|
||||||
|
if seq > self.highest_seen_rx_seq:
|
||||||
|
self.highest_seen_rx_seq = seq
|
||||||
|
if self.highest_seen_rx_seq < self.next_expected_rx_seq:
|
||||||
|
self.highest_seen_rx_seq = 0
|
||||||
|
return ("process", None)
|
||||||
|
|
||||||
|
def remember_out_of_order_frame(self, shell) -> None:
|
||||||
|
with self.tx_lock:
|
||||||
|
if shell.seq <= self.next_expected_rx_seq:
|
||||||
|
return
|
||||||
|
if shell.seq not in self.pending_rx_frames:
|
||||||
|
self.pending_rx_frames[shell.seq] = shell
|
||||||
|
if shell.seq > self.highest_seen_rx_seq:
|
||||||
|
self.highest_seen_rx_seq = shell.seq
|
||||||
|
|
||||||
|
def pop_next_buffered_frame(self):
|
||||||
|
with self.tx_lock:
|
||||||
|
return self.pending_rx_frames.pop(self.next_expected_rx_seq, None)
|
||||||
|
|
||||||
|
def pending_missing_seq(self) -> Optional[int]:
|
||||||
|
with self.tx_lock:
|
||||||
|
if self.highest_seen_rx_seq >= self.next_expected_rx_seq:
|
||||||
|
return self.next_expected_rx_seq
|
||||||
|
return None
|
||||||
|
|
||||||
|
def request_missing_seq_once(self) -> Optional[int]:
|
||||||
|
with self.tx_lock:
|
||||||
|
if self.highest_seen_rx_seq < self.next_expected_rx_seq:
|
||||||
|
return None
|
||||||
|
now = time.monotonic()
|
||||||
|
if (
|
||||||
|
self.last_requested_missing_seq == self.next_expected_rx_seq
|
||||||
|
and (now - self.last_missing_request_time) < MISSING_SEQ_RETRY_INTERVAL_SEC
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
self.last_requested_missing_seq = self.next_expected_rx_seq
|
||||||
|
self.last_missing_request_time = now
|
||||||
|
return self.last_requested_missing_seq
|
||||||
|
|
||||||
|
def set_receive_cursor(self, seq: int) -> None:
|
||||||
|
with self.tx_lock:
|
||||||
|
self.last_rx_seq = seq
|
||||||
|
self.next_expected_rx_seq = seq + 1
|
||||||
|
self.highest_seen_rx_seq = seq
|
||||||
|
|
||||||
|
def open_replay_log(self, session_id: int) -> None:
|
||||||
|
with self.replay_log_lock:
|
||||||
|
if self.replay_log_file is not None:
|
||||||
|
return
|
||||||
|
path = Path.cwd() / f"{session_id:08x}.log"
|
||||||
|
self.replay_log_file = path.open("a", encoding="utf-8")
|
||||||
|
self.replay_log_path = path
|
||||||
|
self.replay_log_file.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} session_open session=0x{session_id:08x}\n")
|
||||||
|
self.replay_log_file.flush()
|
||||||
|
|
||||||
|
def log_replay_event(self, event: str, seq: int, detail: str = "") -> None:
|
||||||
|
with self.replay_log_lock:
|
||||||
|
if self.replay_log_file is None:
|
||||||
|
return
|
||||||
|
extra = f" {detail}" if detail else ""
|
||||||
|
self.replay_log_file.write(
|
||||||
|
f"{time.strftime('%Y-%m-%d %H:%M:%S')} {event} seq={seq}{extra}\n"
|
||||||
|
)
|
||||||
|
self.replay_log_file.flush()
|
||||||
|
|
||||||
|
def note_missing_seq_requested(self, seq: int, reason: str) -> None:
|
||||||
|
with self.tx_lock:
|
||||||
|
self.requested_missing_seqs.add(seq)
|
||||||
|
self.log_replay_event("missing_requested", seq, f"reason={reason}")
|
||||||
|
|
||||||
|
def note_replayed_seq_received(self, seq: int) -> None:
|
||||||
|
with self.tx_lock:
|
||||||
|
was_requested = seq in self.requested_missing_seqs
|
||||||
|
if was_requested:
|
||||||
|
self.requested_missing_seqs.remove(seq)
|
||||||
|
if was_requested:
|
||||||
|
self.log_replay_event("replay_received", seq)
|
||||||
|
|
||||||
|
def close_replay_log(self) -> None:
|
||||||
|
with self.replay_log_lock:
|
||||||
|
if self.replay_log_file is None:
|
||||||
|
return
|
||||||
|
self.replay_log_file.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} session_close\n")
|
||||||
|
self.replay_log_file.flush()
|
||||||
|
self.replay_log_file.close()
|
||||||
|
self.replay_log_file = None
|
||||||
|
|
||||||
|
def remember_sent_frame(self, frame: SentShellFrame) -> None:
|
||||||
|
if frame.seq == 0 or frame.op == self.pb2.mesh.RemoteShell.ACK:
|
||||||
|
return
|
||||||
|
with self.tx_lock:
|
||||||
|
self.tx_history.append(frame)
|
||||||
|
|
||||||
|
def prune_sent_frames(self, ack_seq: int) -> None:
|
||||||
|
if ack_seq <= 0:
|
||||||
|
return
|
||||||
|
with self.tx_lock:
|
||||||
|
self.tx_history = deque((frame for frame in self.tx_history if frame.seq > ack_seq), maxlen=50)
|
||||||
|
|
||||||
|
def replay_frames_from(self, start_seq: int) -> list[SentShellFrame]:
|
||||||
|
with self.tx_lock:
|
||||||
|
return [frame for frame in self.tx_history if frame.seq >= start_seq]
|
||||||
|
|
||||||
|
|
||||||
|
def send_toradio(transport, toradio) -> None:
|
||||||
|
send_stream_frame(transport, toradio.SerializeToString())
|
||||||
|
|
||||||
|
|
||||||
|
def make_toradio_packet(pb2, state: SessionState, shell_msg) -> object:
|
||||||
|
packet = pb2.mesh.MeshPacket()
|
||||||
|
packet.id = random.randint(1, 0x7FFFFFFF)
|
||||||
|
packet.to = state.target
|
||||||
|
# The 'from' field is a reserved keyword in Python, so use setattr
|
||||||
|
setattr(packet, "from", 0)
|
||||||
|
packet.channel = state.channel
|
||||||
|
packet.hop_limit = DEFAULT_HOP_LIMIT
|
||||||
|
packet.want_ack = False
|
||||||
|
packet.decoded.portnum = pb2.portnums.REMOTE_SHELL_APP
|
||||||
|
packet.decoded.payload = shell_msg.SerializeToString()
|
||||||
|
packet.decoded.want_response = False
|
||||||
|
packet.decoded.dest = state.target
|
||||||
|
packet.decoded.source = 0
|
||||||
|
|
||||||
|
toradio = pb2.mesh.ToRadio()
|
||||||
|
toradio.packet.CopyFrom(packet)
|
||||||
|
return toradio
|
||||||
|
|
||||||
|
|
||||||
|
def send_shell_frame(
|
||||||
|
transport,
|
||||||
|
state: SessionState,
|
||||||
|
op: int,
|
||||||
|
payload: bytes = b"",
|
||||||
|
cols: int = 0,
|
||||||
|
rows: int = 0,
|
||||||
|
session_id: Optional[int] = None,
|
||||||
|
ack_seq: Optional[int] = None,
|
||||||
|
seq: Optional[int] = None,
|
||||||
|
flags: int = 0,
|
||||||
|
last_tx_seq: int = 0,
|
||||||
|
last_rx_seq: int = 0,
|
||||||
|
remember: bool = True,
|
||||||
|
heartbeat: bool = False,
|
||||||
|
) -> int:
|
||||||
|
if seq is None:
|
||||||
|
seq = 0 if op == state.pb2.mesh.RemoteShell.ACK else state.alloc_seq()
|
||||||
|
if ack_seq is None:
|
||||||
|
ack_seq = state.current_ack_seq()
|
||||||
|
if session_id is None:
|
||||||
|
session_id = state.session_id
|
||||||
|
|
||||||
|
shell = state.pb2.mesh.RemoteShell()
|
||||||
|
shell.op = op
|
||||||
|
shell.session_id = session_id
|
||||||
|
shell.seq = seq
|
||||||
|
shell.ack_seq = ack_seq
|
||||||
|
shell.cols = cols
|
||||||
|
shell.rows = rows
|
||||||
|
shell.flags = flags
|
||||||
|
shell.last_tx_seq = last_tx_seq
|
||||||
|
shell.last_rx_seq = last_rx_seq
|
||||||
|
if payload:
|
||||||
|
shell.payload = payload
|
||||||
|
with state.socket_lock:
|
||||||
|
send_toradio(transport, make_toradio_packet(state.pb2, state, shell))
|
||||||
|
if remember:
|
||||||
|
state.remember_sent_frame(
|
||||||
|
SentShellFrame(
|
||||||
|
op=op,
|
||||||
|
session_id=session_id,
|
||||||
|
seq=seq,
|
||||||
|
ack_seq=ack_seq,
|
||||||
|
payload=payload,
|
||||||
|
cols=cols,
|
||||||
|
rows=rows,
|
||||||
|
flags=flags,
|
||||||
|
last_tx_seq=last_tx_seq,
|
||||||
|
last_rx_seq=last_rx_seq,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
state.note_outbound_packet(heartbeat=heartbeat)
|
||||||
|
return seq
|
||||||
|
|
||||||
|
|
||||||
|
def send_ack_frame(transport, state: SessionState, replay_from: Optional[int] = None) -> None:
|
||||||
|
send_shell_frame(
|
||||||
|
transport,
|
||||||
|
state,
|
||||||
|
state.pb2.mesh.RemoteShell.ACK,
|
||||||
|
seq=0,
|
||||||
|
last_rx_seq=0 if replay_from is None else replay_from - 1,
|
||||||
|
remember=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def replay_frames_from(transport, state: SessionState, start_seq: int) -> None:
|
||||||
|
frame = next((f for f in state.replay_frames_from(start_seq) if f.seq == start_seq), None)
|
||||||
|
if frame is None:
|
||||||
|
#state.event_queue.put(f"replay unavailable from seq={start_seq}")
|
||||||
|
state.log_replay_event("replay_unavailable", start_seq)
|
||||||
|
return
|
||||||
|
state.log_replay_event("replay_sent", start_seq)
|
||||||
|
#state.event_queue.put(f"replay frame seq={start_seq}")
|
||||||
|
send_shell_frame(
|
||||||
|
transport,
|
||||||
|
state,
|
||||||
|
frame.op,
|
||||||
|
payload=frame.payload,
|
||||||
|
cols=frame.cols,
|
||||||
|
rows=frame.rows,
|
||||||
|
session_id=frame.session_id,
|
||||||
|
ack_seq=frame.ack_seq,
|
||||||
|
seq=frame.seq,
|
||||||
|
flags=frame.flags,
|
||||||
|
last_tx_seq=frame.last_tx_seq,
|
||||||
|
last_rx_seq=frame.last_rx_seq,
|
||||||
|
remember=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_config_complete(transport, pb2, timeout: float, verbose: bool) -> None:
|
||||||
|
nonce = random.randint(1, 0x7FFFFFFF)
|
||||||
|
toradio = pb2.mesh.ToRadio()
|
||||||
|
toradio.want_config_id = nonce
|
||||||
|
send_toradio(transport, toradio)
|
||||||
|
|
||||||
|
deadline = time.time() + timeout
|
||||||
|
while time.time() < deadline:
|
||||||
|
fromradio = pb2.mesh.FromRadio()
|
||||||
|
fromradio.ParseFromString(recv_stream_frame(transport))
|
||||||
|
variant = fromradio.WhichOneof("payload_variant")
|
||||||
|
if verbose and variant:
|
||||||
|
print(f"[api] fromradio {variant}", file=sys.stderr)
|
||||||
|
if variant == "config_complete_id" and fromradio.config_complete_id == nonce:
|
||||||
|
return
|
||||||
|
raise TimeoutError("timed out waiting for config handshake to complete")
|
||||||
|
|
||||||
|
|
||||||
|
def decode_shell_packet(state: SessionState, packet) -> Optional[object]:
|
||||||
|
if packet.WhichOneof("payload_variant") != "decoded":
|
||||||
|
return None
|
||||||
|
if packet.decoded.portnum != state.pb2.portnums.REMOTE_SHELL_APP:
|
||||||
|
return None
|
||||||
|
shell = state.pb2.mesh.RemoteShell()
|
||||||
|
shell.ParseFromString(packet.decoded.payload)
|
||||||
|
return shell
|
||||||
|
|
||||||
|
|
||||||
|
def reader_loop(transport, state: SessionState) -> None:
|
||||||
|
def handle_in_order_shell(shell) -> bool:
|
||||||
|
state.note_replayed_seq_received(shell.seq)
|
||||||
|
if shell.op == state.pb2.mesh.RemoteShell.OPEN_OK:
|
||||||
|
state.session_id = shell.session_id
|
||||||
|
state.open_replay_log(state.session_id)
|
||||||
|
state.set_receive_cursor(shell.seq)
|
||||||
|
state.active = True
|
||||||
|
state.opened_event.set()
|
||||||
|
state.event_queue.put(
|
||||||
|
f"opened session=0x{shell.session_id:08x} cols={shell.cols} rows={shell.rows}"
|
||||||
|
)
|
||||||
|
if state.replay_log_path is not None:
|
||||||
|
state.event_queue.put(f"replay log: {state.replay_log_path}")
|
||||||
|
elif shell.op == state.pb2.mesh.RemoteShell.OUTPUT:
|
||||||
|
if shell.payload:
|
||||||
|
sys.stdout.buffer.write(shell.payload)
|
||||||
|
sys.stdout.buffer.flush()
|
||||||
|
elif shell.op == state.pb2.mesh.RemoteShell.ERROR:
|
||||||
|
message = shell.payload.decode("utf-8", errors="replace")
|
||||||
|
if state.replay_log_file is None:
|
||||||
|
state.open_replay_log(shell.session_id or state.session_id)
|
||||||
|
sanitized = message.replace("\n", "\\n")
|
||||||
|
state.log_replay_event("error_received", shell.seq, f"message={sanitized}")
|
||||||
|
state.event_queue.put(f"remote error: {message}")
|
||||||
|
elif shell.op == state.pb2.mesh.RemoteShell.CLOSED:
|
||||||
|
message = shell.payload.decode("utf-8", errors="replace")
|
||||||
|
state.event_queue.put(f"session closed: {message}")
|
||||||
|
state.closed_event.set()
|
||||||
|
state.active = False
|
||||||
|
return True
|
||||||
|
elif shell.op == state.pb2.mesh.RemoteShell.PONG:
|
||||||
|
remote_last_tx_seq = shell.last_tx_seq
|
||||||
|
remote_last_rx_seq = shell.last_rx_seq
|
||||||
|
local_latest_tx_seq = state.highest_sent_seq()
|
||||||
|
if remote_last_rx_seq != 0 and remote_last_rx_seq < local_latest_tx_seq:
|
||||||
|
replay_frames_from(transport, state, remote_last_rx_seq + 1)
|
||||||
|
if remote_last_tx_seq > state.current_ack_seq():
|
||||||
|
state.note_peer_reported_tx_seq(remote_last_tx_seq)
|
||||||
|
req = state.request_missing_seq_once()
|
||||||
|
if req is not None:
|
||||||
|
state.note_missing_seq_requested(req, "heartbeat_status")
|
||||||
|
send_ack_frame(transport, state, replay_from=req)
|
||||||
|
#state.event_queue.put("pong")
|
||||||
|
return False
|
||||||
|
|
||||||
|
while not state.stopped:
|
||||||
|
try:
|
||||||
|
fromradio = state.pb2.mesh.FromRadio()
|
||||||
|
fromradio.ParseFromString(recv_stream_frame(transport))
|
||||||
|
except Exception as exc:
|
||||||
|
if not state.stopped:
|
||||||
|
state.event_queue.put(f"connection error: {exc}")
|
||||||
|
state.closed_event.set()
|
||||||
|
return
|
||||||
|
|
||||||
|
variant = fromradio.WhichOneof("payload_variant")
|
||||||
|
if variant == "packet":
|
||||||
|
shell = decode_shell_packet(state, fromradio.packet)
|
||||||
|
if not shell:
|
||||||
|
continue
|
||||||
|
state.note_inbound_packet()
|
||||||
|
#state.prune_sent_frames(shell.ack_seq)
|
||||||
|
if shell.op == state.pb2.mesh.RemoteShell.ACK:
|
||||||
|
#state.event_queue.put("peer requested replay")
|
||||||
|
replay_from = shell.last_rx_seq + 1 if shell.last_rx_seq > 0 else None
|
||||||
|
if replay_from is not None:
|
||||||
|
#state.event_queue.put(f"peer requested replay from seq={replay_from}")
|
||||||
|
replay_frames_from(transport, state, replay_from)
|
||||||
|
continue
|
||||||
|
|
||||||
|
action, missing_from = state.note_received_seq(shell.seq)
|
||||||
|
if action == "duplicate":
|
||||||
|
req = state.request_missing_seq_once()
|
||||||
|
if req is not None:
|
||||||
|
state.note_missing_seq_requested(req, "duplicate")
|
||||||
|
send_ack_frame(transport, state, replay_from=req)
|
||||||
|
continue
|
||||||
|
if action == "gap":
|
||||||
|
state.remember_out_of_order_frame(shell)
|
||||||
|
req = state.request_missing_seq_once()
|
||||||
|
if req is not None:
|
||||||
|
state.note_missing_seq_requested(req, "gap")
|
||||||
|
send_ack_frame(transport, state, replay_from=req)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if handle_in_order_shell(shell):
|
||||||
|
return
|
||||||
|
|
||||||
|
while True:
|
||||||
|
buffered_shell = state.pop_next_buffered_frame()
|
||||||
|
if buffered_shell is None:
|
||||||
|
break
|
||||||
|
buffered_action, _ = state.note_received_seq(buffered_shell.seq)
|
||||||
|
if buffered_action != "process":
|
||||||
|
state.remember_out_of_order_frame(buffered_shell)
|
||||||
|
break
|
||||||
|
if handle_in_order_shell(buffered_shell):
|
||||||
|
return
|
||||||
|
|
||||||
|
req = state.request_missing_seq_once()
|
||||||
|
if req is not None:
|
||||||
|
state.note_missing_seq_requested(req, "post_process_gap")
|
||||||
|
send_ack_frame(transport, state, replay_from=req)
|
||||||
|
elif state.verbose and variant:
|
||||||
|
state.event_queue.put(f"fromradio {variant}")
|
||||||
|
|
||||||
|
|
||||||
|
def drain_events(state: SessionState) -> None:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
event = state.event_queue.get_nowait()
|
||||||
|
except queue.Empty:
|
||||||
|
return
|
||||||
|
print(f"[dmshell] {event}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def heartbeat_loop(transport, state: SessionState) -> None:
|
||||||
|
while not state.stopped and not state.closed_event.is_set():
|
||||||
|
if not state.active:
|
||||||
|
time.sleep(HEARTBEAT_POLL_INTERVAL_SEC)
|
||||||
|
continue
|
||||||
|
if state.heartbeat_due():
|
||||||
|
try:
|
||||||
|
send_shell_frame(
|
||||||
|
transport,
|
||||||
|
state,
|
||||||
|
state.pb2.mesh.RemoteShell.PING,
|
||||||
|
last_tx_seq=state.highest_sent_seq(),
|
||||||
|
last_rx_seq=state.current_ack_seq(),
|
||||||
|
remember=True,
|
||||||
|
heartbeat=True,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
if not state.stopped:
|
||||||
|
state.event_queue.put(f"heartbeat error: {exc}")
|
||||||
|
state.closed_event.set()
|
||||||
|
return
|
||||||
|
time.sleep(HEARTBEAT_POLL_INTERVAL_SEC)
|
||||||
|
|
||||||
|
|
||||||
|
def run_command_mode(transport, state: SessionState, commands: list[str], close_after: float) -> None:
|
||||||
|
for command in commands:
|
||||||
|
send_shell_frame(transport, state, state.pb2.mesh.RemoteShell.INPUT, (command + "\n").encode("utf-8"))
|
||||||
|
time.sleep(close_after)
|
||||||
|
send_shell_frame(transport, state, state.pb2.mesh.RemoteShell.CLOSE)
|
||||||
|
state.closed_event.wait(timeout=close_after + 5.0)
|
||||||
|
|
||||||
|
|
||||||
|
def run_interactive_mode(transport, state: SessionState) -> None:
|
||||||
|
def read_local_command() -> str:
|
||||||
|
prompt = "\r\n[dmshell] local command (resume|close|ping|resize C R): "
|
||||||
|
sys.stderr.write(prompt)
|
||||||
|
sys.stderr.flush()
|
||||||
|
buf = bytearray()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
ch = os.read(sys.stdin.fileno(), 1)
|
||||||
|
if not ch:
|
||||||
|
sys.stderr.write("\r\n")
|
||||||
|
sys.stderr.flush()
|
||||||
|
return "close"
|
||||||
|
|
||||||
|
b = ch[0]
|
||||||
|
if b in (10, 13):
|
||||||
|
sys.stderr.write("\r\n")
|
||||||
|
sys.stderr.flush()
|
||||||
|
return buf.decode("utf-8", errors="replace").strip()
|
||||||
|
|
||||||
|
if b in (8, 127):
|
||||||
|
if buf:
|
||||||
|
buf.pop()
|
||||||
|
sys.stderr.write("\b \b")
|
||||||
|
sys.stderr.flush()
|
||||||
|
continue
|
||||||
|
|
||||||
|
if b < 32:
|
||||||
|
continue
|
||||||
|
|
||||||
|
buf.append(b)
|
||||||
|
sys.stderr.write(chr(b))
|
||||||
|
sys.stderr.flush()
|
||||||
|
|
||||||
|
def handle_local_command(cmd: str) -> bool:
|
||||||
|
if cmd in ("", "resume"):
|
||||||
|
return True
|
||||||
|
if cmd == "close":
|
||||||
|
send_shell_frame(transport, state, state.pb2.mesh.RemoteShell.CLOSE)
|
||||||
|
return False
|
||||||
|
if cmd == "ping":
|
||||||
|
send_shell_frame(transport, state, state.pb2.mesh.RemoteShell.PING)
|
||||||
|
return True
|
||||||
|
if cmd.startswith("resize "):
|
||||||
|
parts = cmd.split()
|
||||||
|
if len(parts) != 3:
|
||||||
|
state.event_queue.put("usage: resize COLS ROWS")
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
cols = int(parts[1])
|
||||||
|
rows = int(parts[2])
|
||||||
|
except ValueError:
|
||||||
|
state.event_queue.put("usage: resize COLS ROWS")
|
||||||
|
return True
|
||||||
|
send_shell_frame(transport, state, state.pb2.mesh.RemoteShell.RESIZE, cols=cols, rows=rows)
|
||||||
|
return True
|
||||||
|
|
||||||
|
state.event_queue.put(f"unknown local command: {cmd}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Raw input mode active. All keys (including Ctrl+C/Ctrl+X) are sent to remote. Ctrl+] for local commands.",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not sys.stdin.isatty():
|
||||||
|
# Fallback for non-TTY stdin: still send input as it arrives.
|
||||||
|
while not state.closed_event.is_set():
|
||||||
|
drain_events(state)
|
||||||
|
data = sys.stdin.buffer.read(INPUT_BATCH_MAX_BYTES)
|
||||||
|
if not data:
|
||||||
|
send_shell_frame(transport, state, state.pb2.mesh.RemoteShell.CLOSE)
|
||||||
|
break
|
||||||
|
send_shell_frame(transport, state, state.pb2.mesh.RemoteShell.INPUT, data)
|
||||||
|
return
|
||||||
|
|
||||||
|
fd = sys.stdin.fileno()
|
||||||
|
old_attrs = termios.tcgetattr(fd)
|
||||||
|
try:
|
||||||
|
tty.setraw(fd)
|
||||||
|
while not state.closed_event.is_set():
|
||||||
|
drain_events(state)
|
||||||
|
ready, _, _ = select.select([sys.stdin], [], [], 0.05)
|
||||||
|
if not ready:
|
||||||
|
continue
|
||||||
|
|
||||||
|
data = os.read(fd, 1)
|
||||||
|
if not data:
|
||||||
|
send_shell_frame(transport, state, state.pb2.mesh.RemoteShell.CLOSE)
|
||||||
|
break
|
||||||
|
|
||||||
|
if data == LOCAL_ESCAPE_BYTE:
|
||||||
|
keep_running = handle_local_command(read_local_command())
|
||||||
|
if not keep_running:
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Coalesce a short burst of bytes to reduce packet overhead for fast typing.
|
||||||
|
batched = bytearray(data)
|
||||||
|
enter_local_command = False
|
||||||
|
deadline = time.monotonic() + INPUT_BATCH_WINDOW_SEC
|
||||||
|
while len(batched) < INPUT_BATCH_MAX_BYTES:
|
||||||
|
remaining = deadline - time.monotonic()
|
||||||
|
if remaining <= 0:
|
||||||
|
break
|
||||||
|
more_ready, _, _ = select.select([sys.stdin], [], [], remaining)
|
||||||
|
if not more_ready:
|
||||||
|
break
|
||||||
|
next_byte = os.read(fd, 1)
|
||||||
|
if not next_byte:
|
||||||
|
break
|
||||||
|
if next_byte == LOCAL_ESCAPE_BYTE:
|
||||||
|
enter_local_command = True
|
||||||
|
break
|
||||||
|
batched.extend(next_byte)
|
||||||
|
if next_byte == b'\r' or next_byte == b'\t':
|
||||||
|
break
|
||||||
|
deadline = time.monotonic() + INPUT_BATCH_WINDOW_SEC
|
||||||
|
|
||||||
|
if batched:
|
||||||
|
send_shell_frame(transport, state, state.pb2.mesh.RemoteShell.INPUT, bytes(batched))
|
||||||
|
|
||||||
|
if enter_local_command:
|
||||||
|
keep_running = handle_local_command(read_local_command())
|
||||||
|
if not keep_running:
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
termios.tcsetattr(fd, termios.TCSADRAIN, old_attrs)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = parse_args()
|
||||||
|
pb2 = load_proto_modules()
|
||||||
|
|
||||||
|
state = SessionState(
|
||||||
|
pb2=pb2,
|
||||||
|
target=parse_node_num(args.to),
|
||||||
|
channel=args.channel,
|
||||||
|
verbose=args.verbose,
|
||||||
|
)
|
||||||
|
|
||||||
|
cols, rows = resolve_initial_terminal_size(args.cols, args.rows)
|
||||||
|
|
||||||
|
transport = open_transport(args)
|
||||||
|
try:
|
||||||
|
wait_for_config_complete(transport, pb2, args.timeout, args.verbose)
|
||||||
|
|
||||||
|
reader = threading.Thread(target=reader_loop, args=(transport, state), daemon=True)
|
||||||
|
reader.start()
|
||||||
|
|
||||||
|
send_shell_frame(transport, state, pb2.mesh.RemoteShell.OPEN, cols=cols, rows=rows)
|
||||||
|
if not state.opened_event.wait(timeout=args.timeout):
|
||||||
|
raise SystemExit("timed out waiting for OPEN_OK from remote DMShell")
|
||||||
|
|
||||||
|
heartbeat = threading.Thread(target=heartbeat_loop, args=(transport, state), daemon=True)
|
||||||
|
heartbeat.start()
|
||||||
|
|
||||||
|
drain_events(state)
|
||||||
|
if args.command:
|
||||||
|
run_command_mode(transport, state, args.command, args.close_after)
|
||||||
|
else:
|
||||||
|
run_interactive_mode(transport, state)
|
||||||
|
|
||||||
|
state.stopped = True
|
||||||
|
drain_events(state)
|
||||||
|
reader.join(timeout=1.0)
|
||||||
|
heartbeat.join(timeout=1.0)
|
||||||
|
state.close_replay_log()
|
||||||
|
finally:
|
||||||
|
transport.close()
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -87,9 +87,6 @@
|
|||||||
</screenshots>
|
</screenshots>
|
||||||
|
|
||||||
<releases>
|
<releases>
|
||||||
<release version="2.7.24" date="2026-05-08">
|
|
||||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.24</url>
|
|
||||||
</release>
|
|
||||||
<release version="2.7.23" date="2026-04-14">
|
<release version="2.7.23" date="2026-04-14">
|
||||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.23</url>
|
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.23</url>
|
||||||
</release>
|
</release>
|
||||||
|
|||||||
Vendored
-6
@@ -1,9 +1,3 @@
|
|||||||
meshtasticd (2.7.24.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Version 2.7.24
|
|
||||||
|
|
||||||
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Fri, 08 May 2026 10:44:12 +0000
|
|
||||||
|
|
||||||
meshtasticd (2.7.23.0) unstable; urgency=medium
|
meshtasticd (2.7.23.0) unstable; urgency=medium
|
||||||
|
|
||||||
* Version 2.7.23
|
* Version 2.7.23
|
||||||
|
|||||||
+9
-67
@@ -166,73 +166,15 @@ rather than auto-`sudo`'ing mid-run.
|
|||||||
|
|
||||||
## Environment variables
|
## Environment variables
|
||||||
|
|
||||||
| Var | Default | Purpose |
|
| Var | Default | Purpose |
|
||||||
| -------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
| -------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------- |
|
||||||
| `MESHTASTIC_FIRMWARE_ROOT` | walks up from cwd for `platformio.ini` | Pin the firmware repo |
|
| `MESHTASTIC_FIRMWARE_ROOT` | walks up from cwd for `platformio.ini` | Pin the firmware repo |
|
||||||
| `MESHTASTIC_PIO_BIN` | `~/.platformio/penv/bin/pio` → `$PATH` `pio` → `platformio` | Override `pio` location |
|
| `MESHTASTIC_PIO_BIN` | `~/.platformio/penv/bin/pio` → `$PATH` `pio` → `platformio` | Override `pio` location |
|
||||||
| `MESHTASTIC_ESPTOOL_BIN` | `<firmware>/.venv/bin/esptool` → `$PATH` | Override esptool |
|
| `MESHTASTIC_ESPTOOL_BIN` | `<firmware>/.venv/bin/esptool` → `$PATH` | Override esptool |
|
||||||
| `MESHTASTIC_NRFUTIL_BIN` | `$PATH` | Override nrfutil |
|
| `MESHTASTIC_NRFUTIL_BIN` | `$PATH` | Override nrfutil |
|
||||||
| `MESHTASTIC_PICOTOOL_BIN` | `$PATH` | Override picotool |
|
| `MESHTASTIC_PICOTOOL_BIN` | `$PATH` | Override picotool |
|
||||||
| `MESHTASTIC_MCP_SEED` | `mcp-<user>-<host>` | PSK seed for test-harness session (CI override) |
|
| `MESHTASTIC_MCP_SEED` | `mcp-<user>-<host>` | PSK seed for test-harness session (CI override) |
|
||||||
| `MESHTASTIC_MCP_FLASH_LOG` | `<mcp-server>/tests/flash.log` | Tee target for pio/esptool/nrfutil subprocess output (TUI tails it) |
|
| `MESHTASTIC_MCP_FLASH_LOG` | `<mcp-server>/tests/flash.log` | Tee target for pio/esptool/nrfutil subprocess output (TUI tails it) |
|
||||||
| `MESHTASTIC_MCP_TCP_HOST` | unset | `host` or `host:port` of a `meshtasticd` daemon to surface as a TCP device (see "TCP / native-host nodes" below) |
|
|
||||||
|
|
||||||
## TCP / native-host nodes
|
|
||||||
|
|
||||||
The `native-macos` and `native` PlatformIO envs build a headless `meshtasticd`
|
|
||||||
binary that runs on the host (Apple Silicon / Intel macOS, or Linux Portduino).
|
|
||||||
The daemon exposes the meshtastic TCP API on port `4403` rather than a USB
|
|
||||||
serial endpoint — point the MCP server at it via `MESHTASTIC_MCP_TCP_HOST`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Build + run a daemon on this host (see variants/native/portduino/platformio.ini
|
|
||||||
# for full Homebrew prereqs and CH341 LoRa-adapter setup).
|
|
||||||
pio run -e native-macos
|
|
||||||
~/.meshtasticd/meshtasticd
|
|
||||||
|
|
||||||
# 2. Point the MCP server at it.
|
|
||||||
export MESHTASTIC_MCP_TCP_HOST=localhost # or host:port, default port 4403
|
|
||||||
```
|
|
||||||
|
|
||||||
**First-run gotcha — MAC address.** `meshtasticd` derives its MAC from the
|
|
||||||
USB adapter's serial-number / product strings. Many cheap CH341 dongles
|
|
||||||
(MeshStick included — VID 0x1A86 / PID 0x5512) ship with `iSerialNumber=0`
|
|
||||||
and `iProduct=0`, so the daemon aborts on boot with `*** Blank MAC Address
|
|
||||||
not allowed!`. Set the MAC explicitly in `config.yaml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# Under General:
|
|
||||||
MACAddress: 02:CA:FE:BA:BE:01
|
|
||||||
```
|
|
||||||
|
|
||||||
Use a locally-administered address (first byte's second-LSB set, e.g.
|
|
||||||
`02:*` / `06:*` / `0A:*` / `0E:*`) to avoid colliding with a real OUI.
|
|
||||||
|
|
||||||
There is also a `--hwid AA:BB:CC:DD:EE:FF` CLI flag visible in
|
|
||||||
`meshtasticd --help`, but it is **currently broken** in
|
|
||||||
`MAC_from_string()` (`src/platform/portduino/PortduinoGlue.cpp`): the
|
|
||||||
function strips colons from its parameter but then reads bytes from the
|
|
||||||
global `portduino_config.mac_address`, so `--hwid` is silently overridden
|
|
||||||
when `MACAddress:` is also set, and crashes the daemon (uncaught
|
|
||||||
`std::invalid_argument: stoi: no conversion`) when it isn't. Use the YAML
|
|
||||||
form until that's fixed upstream.
|
|
||||||
|
|
||||||
`list_devices` will surface the daemon as `tcp://localhost:4403` with
|
|
||||||
`likely_meshtastic=True`, so `device_info`, `list_nodes`, `get_config`,
|
|
||||||
`set_config`, `set_owner`, `send_text`, `userprefs_*`, and the admin RPCs
|
|
||||||
auto-select it when no `port` is passed. Pass `port="tcp://other-host:9999"`
|
|
||||||
explicitly to target a different daemon.
|
|
||||||
|
|
||||||
**Tools that don't apply to a TCP/native node** (no USB hardware to operate
|
|
||||||
on) raise a clear `ConnectionError` rather than failing mysteriously:
|
|
||||||
`pio_flash`, `erase_and_flash`, `update_flash`, `touch_1200bps`,
|
|
||||||
`serial_open` (use info/admin tools directly), and the vendor escape hatches
|
|
||||||
`esptool_*`, `nrfutil_*`, `picotool_*`. `pio_flash` against a `native*` env
|
|
||||||
similarly raises — there's no upload step; use `build` and run the binary
|
|
||||||
directly.
|
|
||||||
|
|
||||||
The pytest harness in `tests/` still assumes USB-attached devices per role —
|
|
||||||
TCP-aware fixtures are not part of this surface yet.
|
|
||||||
|
|
||||||
## Hardware Test Suite
|
## Hardware Test Suite
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Context manager for meshtastic interface connections (serial + TCP).
|
"""Context manager for meshtastic.SerialInterface connections.
|
||||||
|
|
||||||
Every info/admin tool goes through `connect(port)` so we have a single place
|
Every info/admin tool goes through `connect(port)` so we have a single place
|
||||||
that:
|
that:
|
||||||
@@ -6,16 +6,8 @@ that:
|
|||||||
- fails fast if a serial_session is already holding the port,
|
- fails fast if a serial_session is already holding the port,
|
||||||
- guarantees `.close()` is called, even on exception.
|
- guarantees `.close()` is called, even on exception.
|
||||||
|
|
||||||
Two transports:
|
The `SerialInterface` blocks on construction waiting for the node database;
|
||||||
- Serial: USB-attached firmware on `/dev/cu.*` / `/dev/ttyUSB*` / `COM*`.
|
that's fine for v1 since every tool is a short-lived request.
|
||||||
- TCP: a `meshtasticd` daemon (e.g. the native macOS / Linux Portduino
|
|
||||||
headless build) addressed as `tcp://host[:port]` (default port 4403).
|
|
||||||
Surfaced by `devices.list_devices()` when `MESHTASTIC_MCP_TCP_HOST` is
|
|
||||||
set, so `resolve_port(None)` auto-selects it like a USB candidate.
|
|
||||||
|
|
||||||
Both `SerialInterface` and `TCPInterface` block on construction waiting for
|
|
||||||
the node database; that's fine for v1 since every tool is a short-lived
|
|
||||||
request.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -25,107 +17,20 @@ from typing import Iterator
|
|||||||
|
|
||||||
from . import devices, registry
|
from . import devices, registry
|
||||||
|
|
||||||
DEFAULT_TCP_PORT = 4403
|
|
||||||
TCP_SCHEME = "tcp://"
|
|
||||||
TCP_HOST_ENV = "MESHTASTIC_MCP_TCP_HOST"
|
|
||||||
|
|
||||||
|
|
||||||
class ConnectionError(RuntimeError):
|
class ConnectionError(RuntimeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def is_tcp_port(port: str | None) -> bool:
|
|
||||||
return bool(port) and port.startswith(TCP_SCHEME)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_tcp_port(port: str) -> tuple[str, int]:
|
|
||||||
"""Parse `tcp://host[:port]` → (host, port). Defaults to 4403.
|
|
||||||
|
|
||||||
Validates host shape (non-empty, no path separators) and port range
|
|
||||||
(1..65535). Raises `ConnectionError` on malformed input — never lets
|
|
||||||
a raw `ValueError` bubble up to a tool surface.
|
|
||||||
"""
|
|
||||||
if not port.startswith(TCP_SCHEME):
|
|
||||||
raise ConnectionError(
|
|
||||||
f"Invalid TCP endpoint {port!r}: expected '{TCP_SCHEME}host[:port]'."
|
|
||||||
)
|
|
||||||
rest = port[len(TCP_SCHEME) :]
|
|
||||||
if ":" in rest:
|
|
||||||
host, port_str = rest.rsplit(":", 1)
|
|
||||||
try:
|
|
||||||
tcp_port = int(port_str)
|
|
||||||
except ValueError as e:
|
|
||||||
raise ConnectionError(
|
|
||||||
f"Invalid TCP endpoint {port!r}: port {port_str!r} is not an integer."
|
|
||||||
) from e
|
|
||||||
else:
|
|
||||||
host, tcp_port = rest, DEFAULT_TCP_PORT
|
|
||||||
if not host:
|
|
||||||
raise ConnectionError(f"Invalid TCP endpoint {port!r}: empty host.")
|
|
||||||
if any(c in host for c in ("/", "\\")):
|
|
||||||
raise ConnectionError(
|
|
||||||
f"Invalid TCP endpoint {port!r}: host {host!r} contains a path "
|
|
||||||
"separator. TCP hostnames cannot contain '/' or '\\' — did you "
|
|
||||||
"pass a serial port path or a Windows drive path by mistake?"
|
|
||||||
)
|
|
||||||
if not (1 <= tcp_port <= 65535):
|
|
||||||
raise ConnectionError(
|
|
||||||
f"Invalid TCP endpoint {port!r}: port {tcp_port} out of range "
|
|
||||||
"(must be 1..65535)."
|
|
||||||
)
|
|
||||||
return host, tcp_port
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_tcp_endpoint(endpoint: str) -> str:
|
|
||||||
r"""Normalize `host`, `host:port`, or `tcp://host[:port]` → canonical
|
|
||||||
`tcp://host:port` form. One place that owns the lock-key shape.
|
|
||||||
|
|
||||||
Defers all validation to `parse_tcp_port`, so path-like inputs
|
|
||||||
(`/dev/cu.foo`, `C:\Windows\…`), empty hosts, non-integer ports,
|
|
||||||
and out-of-range ports raise `ConnectionError` here too.
|
|
||||||
"""
|
|
||||||
if endpoint.startswith(TCP_SCHEME):
|
|
||||||
canonical = endpoint
|
|
||||||
elif ":" in endpoint:
|
|
||||||
canonical = f"{TCP_SCHEME}{endpoint}"
|
|
||||||
else:
|
|
||||||
canonical = f"{TCP_SCHEME}{endpoint}:{DEFAULT_TCP_PORT}"
|
|
||||||
host, port = parse_tcp_port(canonical)
|
|
||||||
return f"{TCP_SCHEME}{host}:{port}"
|
|
||||||
|
|
||||||
|
|
||||||
def reject_if_tcp(port: str | None, tool_name: str) -> None:
|
|
||||||
"""Raise if `port` is a TCP endpoint — for tools that need real USB
|
|
||||||
hardware (flash, bootloader, vendor escape hatches, serial monitor).
|
|
||||||
|
|
||||||
Only checks the explicit arg; auto-selection via env var is the caller's
|
|
||||||
responsibility to handle if it matters.
|
|
||||||
"""
|
|
||||||
if is_tcp_port(port):
|
|
||||||
raise ConnectionError(
|
|
||||||
f"{tool_name} is not applicable to TCP/native nodes ({port}). "
|
|
||||||
"This tool requires USB-attached hardware."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_port(port: str | None) -> str:
|
def resolve_port(port: str | None) -> str:
|
||||||
"""Pick a port: explicit > sole likely_meshtastic candidate > error.
|
"""Pick a port: explicit > sole likely_meshtastic candidate > error."""
|
||||||
|
|
||||||
A `tcp://` string passes through (after canonicalization). When `port`
|
|
||||||
is None and no USB candidates are present, `MESHTASTIC_MCP_TCP_HOST`
|
|
||||||
is consulted via `devices.list_devices()`.
|
|
||||||
"""
|
|
||||||
if port:
|
if port:
|
||||||
if is_tcp_port(port):
|
|
||||||
return normalize_tcp_endpoint(port)
|
|
||||||
return port
|
return port
|
||||||
candidates = [d for d in devices.list_devices() if d["likely_meshtastic"]]
|
candidates = [d for d in devices.list_devices() if d["likely_meshtastic"]]
|
||||||
if not candidates:
|
if not candidates:
|
||||||
raise ConnectionError(
|
raise ConnectionError(
|
||||||
"No Meshtastic devices detected. Plug one in, set "
|
"No Meshtastic devices detected. Plug one in or pass `port` explicitly. "
|
||||||
f"{TCP_HOST_ENV}=<host[:port]> for a meshtasticd daemon, "
|
"Run `list_devices` with include_unknown=True to see all serial ports."
|
||||||
"or pass `port` explicitly. Run `list_devices` with "
|
|
||||||
"include_unknown=True to see all serial ports."
|
|
||||||
)
|
)
|
||||||
if len(candidates) > 1:
|
if len(candidates) > 1:
|
||||||
ports = ", ".join(c["port"] for c in candidates)
|
ports = ", ".join(c["port"] for c in candidates)
|
||||||
@@ -138,62 +43,17 @@ def resolve_port(port: str | None) -> str:
|
|||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def connect(port: str | None = None, timeout_s: float = 8.0) -> Iterator:
|
def connect(port: str | None = None, timeout_s: float = 8.0) -> Iterator:
|
||||||
"""Open a meshtastic interface (serial or TCP) and always close it.
|
"""Open a `meshtastic.SerialInterface` and always close it.
|
||||||
|
|
||||||
For serial: raises `ConnectionError` immediately if another serial
|
Raises `ConnectionError` immediately if another serial session holds the
|
||||||
session holds the port (a `pio device monitor` in `serial_sessions/`).
|
port (a `pio device monitor` in `serial_sessions/`, for instance).
|
||||||
For TCP: no exclusive-access requirement, so the serial-session check
|
|
||||||
is skipped — but the `port_lock` still serializes parallel `connect()`
|
|
||||||
calls to the same daemon endpoint.
|
|
||||||
|
|
||||||
`timeout_s` is plumbed through to both `SerialInterface(timeout=...)`
|
|
||||||
and `TCPInterface(timeout=...)`. The meshtastic library uses the value
|
|
||||||
as the reply-wait deadline for `localNode.waitForConfig()` during
|
|
||||||
construction and for any subsequent admin RPC. `int()`-converted at
|
|
||||||
the boundary because the upstream API expects whole seconds.
|
|
||||||
"""
|
"""
|
||||||
resolved = resolve_port(port)
|
|
||||||
timeout = int(timeout_s)
|
|
||||||
|
|
||||||
if is_tcp_port(resolved):
|
|
||||||
from meshtastic.tcp_interface import (
|
|
||||||
TCPInterface, # type: ignore[import-untyped]
|
|
||||||
)
|
|
||||||
|
|
||||||
host, tcp_port = parse_tcp_port(resolved)
|
|
||||||
lock = registry.port_lock(resolved)
|
|
||||||
if not lock.acquire(blocking=False):
|
|
||||||
raise ConnectionError(
|
|
||||||
f"TCP endpoint {resolved} is busy — another device operation "
|
|
||||||
"is in flight. Retry shortly."
|
|
||||||
)
|
|
||||||
|
|
||||||
iface = None
|
|
||||||
try:
|
|
||||||
iface = TCPInterface(
|
|
||||||
hostname=host,
|
|
||||||
portNumber=tcp_port,
|
|
||||||
connectNow=True,
|
|
||||||
noProto=False,
|
|
||||||
timeout=timeout,
|
|
||||||
)
|
|
||||||
yield iface
|
|
||||||
finally:
|
|
||||||
if iface is not None:
|
|
||||||
try:
|
|
||||||
iface.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
lock.release()
|
|
||||||
except RuntimeError:
|
|
||||||
pass
|
|
||||||
return
|
|
||||||
|
|
||||||
from meshtastic.serial_interface import (
|
from meshtastic.serial_interface import (
|
||||||
SerialInterface, # type: ignore[import-untyped]
|
SerialInterface, # type: ignore[import-untyped]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
resolved = resolve_port(port)
|
||||||
|
|
||||||
active = registry.active_session_for_port(resolved)
|
active = registry.active_session_for_port(resolved)
|
||||||
if active is not None:
|
if active is not None:
|
||||||
raise ConnectionError(
|
raise ConnectionError(
|
||||||
@@ -210,12 +70,7 @@ def connect(port: str | None = None, timeout_s: float = 8.0) -> Iterator:
|
|||||||
|
|
||||||
iface = None
|
iface = None
|
||||||
try:
|
try:
|
||||||
iface = SerialInterface(
|
iface = SerialInterface(devPath=resolved, connectNow=True, noProto=False)
|
||||||
devPath=resolved,
|
|
||||||
connectNow=True,
|
|
||||||
noProto=False,
|
|
||||||
timeout=timeout,
|
|
||||||
)
|
|
||||||
yield iface
|
yield iface
|
||||||
finally:
|
finally:
|
||||||
if iface is not None:
|
if iface is not None:
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
"""USB/serial + TCP device discovery.
|
"""USB/serial device discovery.
|
||||||
|
|
||||||
Combines the canonical `meshtastic.util.findPorts()` allowlist/blocklist with
|
Combines the canonical `meshtastic.util.findPorts()` allowlist/blocklist with
|
||||||
the richer metadata (`serial.tools.list_ports.comports()`) so callers see
|
the richer metadata (`serial.tools.list_ports.comports()`) so callers see
|
||||||
VID/PID, descriptions, and manufacturer strings alongside the "is this likely
|
VID/PID, descriptions, and manufacturer strings alongside the "is this likely
|
||||||
a Meshtastic device" signal.
|
a Meshtastic device" signal.
|
||||||
|
|
||||||
If `MESHTASTIC_MCP_TCP_HOST=<host[:port]>` is set, a synthetic entry for the
|
|
||||||
`meshtasticd` daemon at that endpoint is prepended to the result, so
|
|
||||||
`resolve_port(None)` auto-selects it like a USB candidate.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from serial.tools import list_ports
|
from serial.tools import list_ports
|
||||||
@@ -24,45 +19,6 @@ def _to_hex(value: int | None) -> str | None:
|
|||||||
return f"0x{value:04x}"
|
return f"0x{value:04x}"
|
||||||
|
|
||||||
|
|
||||||
def _tcp_endpoint_from_env() -> dict[str, Any] | None:
|
|
||||||
"""Synthesize a TCP device entry from MESHTASTIC_MCP_TCP_HOST, if set.
|
|
||||||
|
|
||||||
If the env var is malformed (non-integer port, path-like host, etc.),
|
|
||||||
return an entry with `likely_meshtastic=False` and the parser error in
|
|
||||||
the description, rather than raising — `list_devices` is the diagnostic
|
|
||||||
tool a user reaches for when their env var isn't working, so it must
|
|
||||||
not crash on misconfiguration.
|
|
||||||
"""
|
|
||||||
host = os.environ.get("MESHTASTIC_MCP_TCP_HOST")
|
|
||||||
if not host:
|
|
||||||
return None
|
|
||||||
# Lazy import to avoid a circular dependency (connection imports devices).
|
|
||||||
from . import connection
|
|
||||||
|
|
||||||
try:
|
|
||||||
port = connection.normalize_tcp_endpoint(host)
|
|
||||||
description = "meshtasticd (TCP)"
|
|
||||||
likely = True
|
|
||||||
except connection.ConnectionError as e:
|
|
||||||
# Surface the raw env-var value plus the parser's reason so the
|
|
||||||
# user can see exactly what they set and why it was rejected.
|
|
||||||
# Don't double the scheme if the user already prefixed `tcp://`.
|
|
||||||
port = host if host.startswith(connection.TCP_SCHEME) else f"tcp://{host}"
|
|
||||||
description = f"meshtasticd (TCP) — invalid MESHTASTIC_MCP_TCP_HOST: {e}"
|
|
||||||
likely = False
|
|
||||||
return {
|
|
||||||
"port": port,
|
|
||||||
"vid": None,
|
|
||||||
"pid": None,
|
|
||||||
"description": description,
|
|
||||||
"manufacturer": None,
|
|
||||||
"product": None,
|
|
||||||
"serial_number": None,
|
|
||||||
"likely_meshtastic": likely,
|
|
||||||
"blacklisted": False,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def list_devices(include_unknown: bool = False) -> list[dict[str, Any]]:
|
def list_devices(include_unknown: bool = False) -> list[dict[str, Any]]:
|
||||||
"""Return enriched info for serial ports, flagging Meshtastic candidates.
|
"""Return enriched info for serial ports, flagging Meshtastic candidates.
|
||||||
|
|
||||||
@@ -114,22 +70,6 @@ def list_devices(include_unknown: bool = False) -> list[dict[str, Any]]:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Append the TCP endpoint (if env var set) and sort everything together.
|
# Stable ordering: likely_meshtastic first, then by port path
|
||||||
tcp_entry = _tcp_endpoint_from_env()
|
results.sort(key=lambda r: (not r["likely_meshtastic"], r["port"]))
|
||||||
if tcp_entry is not None:
|
|
||||||
results.append(tcp_entry)
|
|
||||||
|
|
||||||
# Stable ordering: likely_meshtastic first; within rank, TCP wins over
|
|
||||||
# USB (explicit env-var configuration takes precedence over USB
|
|
||||||
# enumeration); then by port path. A misconfigured TCP entry has
|
|
||||||
# likely_meshtastic=False and lands among the other ignored entries —
|
|
||||||
# it does NOT pre-empt real USB devices at the top of the list.
|
|
||||||
results.sort(
|
|
||||||
key=lambda r: (
|
|
||||||
not r["likely_meshtastic"],
|
|
||||||
not r["port"].startswith("tcp://"),
|
|
||||||
r["port"],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from typing import Any
|
|||||||
|
|
||||||
import serial
|
import serial
|
||||||
|
|
||||||
from . import boards, config, connection, devices, pio, userprefs
|
from . import boards, config, devices, pio, userprefs
|
||||||
|
|
||||||
# Meshtastic variants use both `esp32s3` and `esp32-s3` style names across
|
# Meshtastic variants use both `esp32s3` and `esp32-s3` style names across
|
||||||
# variants/*/platformio.ini (no consistency enforced). Accept both spellings.
|
# variants/*/platformio.ini (no consistency enforced). Accept both spellings.
|
||||||
@@ -46,18 +46,6 @@ def _require_confirm(confirm: bool, operation: str) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _reject_native_env(env: str, operation: str) -> None:
|
|
||||||
"""`native*` envs build a host executable, not firmware — there's no
|
|
||||||
upload step. The user wants `build` (or just runs the binary directly).
|
|
||||||
"""
|
|
||||||
if env.startswith("native"):
|
|
||||||
raise FlashError(
|
|
||||||
f"{operation} is not applicable for env {env!r}: native envs "
|
|
||||||
"produce a host executable, not flashable firmware. Use `build` "
|
|
||||||
"instead, then run the resulting binary directly."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _artifacts_for(env: str) -> list[Path]:
|
def _artifacts_for(env: str) -> list[Path]:
|
||||||
build_dir = config.firmware_root() / ".pio" / "build" / env
|
build_dir = config.firmware_root() / ".pio" / "build" / env
|
||||||
if not build_dir.is_dir():
|
if not build_dir.is_dir():
|
||||||
@@ -153,8 +141,6 @@ def flash(
|
|||||||
that pio performs will pick up the injected values.
|
that pio performs will pick up the injected values.
|
||||||
"""
|
"""
|
||||||
_require_confirm(confirm, "flash")
|
_require_confirm(confirm, "flash")
|
||||||
_reject_native_env(env, "flash")
|
|
||||||
connection.reject_if_tcp(port, "flash")
|
|
||||||
with userprefs.temporary_overrides(userprefs_overrides) as effective:
|
with userprefs.temporary_overrides(userprefs_overrides) as effective:
|
||||||
result = pio.run(
|
result = pio.run(
|
||||||
["run", "-e", env, "-t", "upload", "--upload-port", port],
|
["run", "-e", env, "-t", "upload", "--upload-port", port],
|
||||||
@@ -214,7 +200,6 @@ def erase_and_flash(
|
|||||||
in that case) since a cached factory.bin would not reflect the new prefs.
|
in that case) since a cached factory.bin would not reflect the new prefs.
|
||||||
"""
|
"""
|
||||||
_require_confirm(confirm, "erase_and_flash")
|
_require_confirm(confirm, "erase_and_flash")
|
||||||
connection.reject_if_tcp(port, "erase_and_flash")
|
|
||||||
_check_esp32_env(env)
|
_check_esp32_env(env)
|
||||||
|
|
||||||
if userprefs_overrides and skip_build:
|
if userprefs_overrides and skip_build:
|
||||||
@@ -272,7 +257,6 @@ def update_flash(
|
|||||||
overrides are provided we always force a rebuild.
|
overrides are provided we always force a rebuild.
|
||||||
"""
|
"""
|
||||||
_require_confirm(confirm, "update_flash")
|
_require_confirm(confirm, "update_flash")
|
||||||
connection.reject_if_tcp(port, "update_flash")
|
|
||||||
_check_esp32_env(env)
|
_check_esp32_env(env)
|
||||||
|
|
||||||
if userprefs_overrides and skip_build:
|
if userprefs_overrides and skip_build:
|
||||||
@@ -407,7 +391,6 @@ def touch_1200bps(
|
|||||||
|
|
||||||
Returns `{ok, former_port, new_port, new_port_vid_pid, attempts}`.
|
Returns `{ok, former_port, new_port, new_port_vid_pid, attempts}`.
|
||||||
"""
|
"""
|
||||||
connection.reject_if_tcp(port, "touch_1200bps")
|
|
||||||
before_list = devices.list_devices(include_unknown=True)
|
before_list = devices.list_devices(include_unknown=True)
|
||||||
before_ports = {d["port"] for d in before_list}
|
before_ports = {d["port"] for d in before_list}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import subprocess
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Sequence
|
from typing import Any, Sequence
|
||||||
|
|
||||||
from . import config, connection, pio
|
from . import config, pio
|
||||||
|
|
||||||
_TIMEOUT_SHORT = 30
|
_TIMEOUT_SHORT = 30
|
||||||
_TIMEOUT_LONG = 600
|
_TIMEOUT_LONG = 600
|
||||||
@@ -102,7 +102,6 @@ def _parse_esptool_chip_info(stdout: str) -> dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
def esptool_chip_info(port: str) -> dict[str, Any]:
|
def esptool_chip_info(port: str) -> dict[str, Any]:
|
||||||
connection.reject_if_tcp(port, "esptool_chip_info")
|
|
||||||
binary = config.esptool_bin()
|
binary = config.esptool_bin()
|
||||||
# `chip_id` prints chip + mac + crystal + features. `flash_id` adds flash.
|
# `chip_id` prints chip + mac + crystal + features. `flash_id` adds flash.
|
||||||
combined = _run(binary, ["--port", port, "flash_id"], timeout=_TIMEOUT_SHORT)
|
combined = _run(binary, ["--port", port, "flash_id"], timeout=_TIMEOUT_SHORT)
|
||||||
@@ -117,7 +116,6 @@ def esptool_chip_info(port: str) -> dict[str, Any]:
|
|||||||
def esptool_erase_flash(port: str, confirm: bool = False) -> dict[str, Any]:
|
def esptool_erase_flash(port: str, confirm: bool = False) -> dict[str, Any]:
|
||||||
"""Full-chip erase. Leaves the device unbootable until reflashed."""
|
"""Full-chip erase. Leaves the device unbootable until reflashed."""
|
||||||
_require_confirm(confirm, "esptool_erase_flash")
|
_require_confirm(confirm, "esptool_erase_flash")
|
||||||
connection.reject_if_tcp(port, "esptool_erase_flash")
|
|
||||||
binary = config.esptool_bin()
|
binary = config.esptool_bin()
|
||||||
# esptool v5 uses `erase-flash`, older uses `erase_flash`. Try the new name
|
# esptool v5 uses `erase-flash`, older uses `erase_flash`. Try the new name
|
||||||
# first; if it fails with unknown command, retry old.
|
# first; if it fails with unknown command, retry old.
|
||||||
@@ -136,7 +134,6 @@ def esptool_raw(
|
|||||||
"""Raw esptool passthrough. Destructive subcommands require confirm=True."""
|
"""Raw esptool passthrough. Destructive subcommands require confirm=True."""
|
||||||
if not args:
|
if not args:
|
||||||
raise ToolError("args must not be empty")
|
raise ToolError("args must not be empty")
|
||||||
connection.reject_if_tcp(port, "esptool_raw")
|
|
||||||
# Find the first non-flag arg (the subcommand).
|
# Find the first non-flag arg (the subcommand).
|
||||||
subcommand = next((a for a in args if not a.startswith("-")), None)
|
subcommand = next((a for a in args if not a.startswith("-")), None)
|
||||||
if subcommand and subcommand.replace("-", "_") in {
|
if subcommand and subcommand.replace("-", "_") in {
|
||||||
@@ -159,7 +156,6 @@ NRFUTIL_DESTRUCTIVE = {"dfu", "settings"}
|
|||||||
|
|
||||||
def nrfutil_dfu(port: str, package_path: str, confirm: bool = False) -> dict[str, Any]:
|
def nrfutil_dfu(port: str, package_path: str, confirm: bool = False) -> dict[str, Any]:
|
||||||
_require_confirm(confirm, "nrfutil_dfu")
|
_require_confirm(confirm, "nrfutil_dfu")
|
||||||
connection.reject_if_tcp(port, "nrfutil_dfu")
|
|
||||||
pkg = Path(package_path).expanduser()
|
pkg = Path(package_path).expanduser()
|
||||||
if not pkg.is_file():
|
if not pkg.is_file():
|
||||||
raise ToolError(f"Package not found: {pkg}")
|
raise ToolError(f"Package not found: {pkg}")
|
||||||
@@ -217,7 +213,6 @@ def _parse_picotool_info(stdout: str) -> dict[str, Any]:
|
|||||||
def picotool_info(port: str | None = None) -> dict[str, Any]:
|
def picotool_info(port: str | None = None) -> dict[str, Any]:
|
||||||
"""Read device info from a Pico in BOOTSEL mode. `port` is informational
|
"""Read device info from a Pico in BOOTSEL mode. `port` is informational
|
||||||
only — picotool auto-detects."""
|
only — picotool auto-detects."""
|
||||||
connection.reject_if_tcp(port, "picotool_info")
|
|
||||||
binary = config.picotool_bin()
|
binary = config.picotool_bin()
|
||||||
res = _run(binary, ["info", "-a"], timeout=_TIMEOUT_SHORT)
|
res = _run(binary, ["info", "-a"], timeout=_TIMEOUT_SHORT)
|
||||||
if res["exit_code"] != 0:
|
if res["exit_code"] != 0:
|
||||||
|
|||||||
@@ -71,10 +71,6 @@ def open_session(
|
|||||||
If `env` is supplied, pio resolves baud and filters from platformio.ini.
|
If `env` is supplied, pio resolves baud and filters from platformio.ini.
|
||||||
Otherwise uses the supplied `baud` and `filters` (default `['direct']`).
|
Otherwise uses the supplied `baud` and `filters` (default `['direct']`).
|
||||||
"""
|
"""
|
||||||
# Lazy import to avoid circular: registry imports serial_session.
|
|
||||||
from . import connection
|
|
||||||
|
|
||||||
connection.reject_if_tcp(port, "serial_open")
|
|
||||||
args = ["device", "monitor", "--port", port, "--no-reconnect"]
|
args = ["device", "monitor", "--port", port, "--no-reconnect"]
|
||||||
effective_filters: list[str]
|
effective_filters: list[str]
|
||||||
effective_baud: int = baud
|
effective_baud: int = baud
|
||||||
|
|||||||
@@ -1,383 +0,0 @@
|
|||||||
"""TCP transport plumbing in connection.py + devices.py.
|
|
||||||
|
|
||||||
Pure-Python tests — no real device or daemon required. Mocks `TCPInterface`
|
|
||||||
when exercising `connect()`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from meshtastic_mcp import connection, devices
|
|
||||||
|
|
||||||
# ---------- helpers --------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class TestIsTcpPort:
|
|
||||||
def test_tcp_scheme(self) -> None:
|
|
||||||
assert connection.is_tcp_port("tcp://localhost") is True
|
|
||||||
assert connection.is_tcp_port("tcp://localhost:4403") is True
|
|
||||||
assert connection.is_tcp_port("tcp://192.168.1.50:9999") is True
|
|
||||||
|
|
||||||
def test_serial_paths(self) -> None:
|
|
||||||
assert connection.is_tcp_port("/dev/cu.usbmodem1234") is False
|
|
||||||
assert connection.is_tcp_port("/dev/ttyUSB0") is False
|
|
||||||
assert connection.is_tcp_port("COM3") is False
|
|
||||||
|
|
||||||
def test_empty_or_none(self) -> None:
|
|
||||||
assert connection.is_tcp_port(None) is False
|
|
||||||
assert connection.is_tcp_port("") is False
|
|
||||||
|
|
||||||
|
|
||||||
class TestParseTcpPort:
|
|
||||||
def test_default_port(self) -> None:
|
|
||||||
assert connection.parse_tcp_port("tcp://localhost") == ("localhost", 4403)
|
|
||||||
|
|
||||||
def test_explicit_port(self) -> None:
|
|
||||||
assert connection.parse_tcp_port("tcp://localhost:9999") == (
|
|
||||||
"localhost",
|
|
||||||
9999,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_ip_with_port(self) -> None:
|
|
||||||
assert connection.parse_tcp_port("tcp://192.168.1.50:4403") == (
|
|
||||||
"192.168.1.50",
|
|
||||||
4403,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestNormalizeTcpEndpoint:
|
|
||||||
def test_bare_host(self) -> None:
|
|
||||||
assert connection.normalize_tcp_endpoint("localhost") == "tcp://localhost:4403"
|
|
||||||
|
|
||||||
def test_host_port(self) -> None:
|
|
||||||
assert (
|
|
||||||
connection.normalize_tcp_endpoint("localhost:5000")
|
|
||||||
== "tcp://localhost:5000"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_full_url(self) -> None:
|
|
||||||
assert (
|
|
||||||
connection.normalize_tcp_endpoint("tcp://1.2.3.4") == "tcp://1.2.3.4:4403"
|
|
||||||
)
|
|
||||||
assert (
|
|
||||||
connection.normalize_tcp_endpoint("tcp://1.2.3.4:9999")
|
|
||||||
== "tcp://1.2.3.4:9999"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_idempotent(self) -> None:
|
|
||||||
once = connection.normalize_tcp_endpoint("localhost:4403")
|
|
||||||
twice = connection.normalize_tcp_endpoint(once)
|
|
||||||
assert once == twice == "tcp://localhost:4403"
|
|
||||||
|
|
||||||
def test_path_like_endpoint_rejected(self) -> None:
|
|
||||||
# Serial port paths and Windows drive paths are common config typos
|
|
||||||
# (someone passes a serial path to MESHTASTIC_MCP_TCP_HOST). Reject
|
|
||||||
# rather than producing a nonsense `tcp:///dev/cu.foo:4403` URL.
|
|
||||||
with pytest.raises(connection.ConnectionError, match="path separator"):
|
|
||||||
connection.normalize_tcp_endpoint("/dev/cu.foo")
|
|
||||||
with pytest.raises(connection.ConnectionError):
|
|
||||||
connection.normalize_tcp_endpoint("tcp:///dev/cu.foo:4403")
|
|
||||||
with pytest.raises(connection.ConnectionError):
|
|
||||||
connection.normalize_tcp_endpoint(r"C:\Windows\System32")
|
|
||||||
|
|
||||||
def test_non_integer_port_rejected(self) -> None:
|
|
||||||
with pytest.raises(connection.ConnectionError, match="not an integer"):
|
|
||||||
connection.normalize_tcp_endpoint("tcp://host:notaport")
|
|
||||||
with pytest.raises(connection.ConnectionError, match="not an integer"):
|
|
||||||
connection.normalize_tcp_endpoint("host:notaport")
|
|
||||||
|
|
||||||
def test_empty_host_rejected(self) -> None:
|
|
||||||
with pytest.raises(connection.ConnectionError, match="empty host"):
|
|
||||||
connection.normalize_tcp_endpoint("tcp://:4403")
|
|
||||||
|
|
||||||
def test_port_out_of_range_rejected(self) -> None:
|
|
||||||
with pytest.raises(connection.ConnectionError, match="out of range"):
|
|
||||||
connection.normalize_tcp_endpoint("tcp://host:0")
|
|
||||||
with pytest.raises(connection.ConnectionError, match="out of range"):
|
|
||||||
connection.normalize_tcp_endpoint("tcp://host:65536")
|
|
||||||
with pytest.raises(connection.ConnectionError, match="out of range"):
|
|
||||||
connection.normalize_tcp_endpoint("host:99999")
|
|
||||||
|
|
||||||
|
|
||||||
class TestParseTcpPortValidation:
|
|
||||||
def test_missing_scheme_rejected(self) -> None:
|
|
||||||
# parse_tcp_port is a low-level helper that requires the scheme.
|
|
||||||
# Misuse should fail loudly rather than silently mis-parsing.
|
|
||||||
with pytest.raises(connection.ConnectionError, match="expected"):
|
|
||||||
connection.parse_tcp_port("localhost:4403")
|
|
||||||
|
|
||||||
def test_negative_port_rejected(self) -> None:
|
|
||||||
with pytest.raises(connection.ConnectionError, match="out of range"):
|
|
||||||
connection.parse_tcp_port("tcp://host:-1")
|
|
||||||
|
|
||||||
|
|
||||||
# ---------- reject_if_tcp --------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class TestRejectIfTcp:
|
|
||||||
def test_rejects_tcp(self) -> None:
|
|
||||||
with pytest.raises(connection.ConnectionError, match="not applicable"):
|
|
||||||
connection.reject_if_tcp("tcp://localhost", "esptool_chip_info")
|
|
||||||
|
|
||||||
def test_passes_through_serial(self) -> None:
|
|
||||||
connection.reject_if_tcp("/dev/cu.usbmodem1", "esptool_chip_info") # no raise
|
|
||||||
|
|
||||||
def test_passes_through_none(self) -> None:
|
|
||||||
# None means "auto-detect"; not the explicit-arg case we guard.
|
|
||||||
connection.reject_if_tcp(None, "esptool_chip_info") # no raise
|
|
||||||
|
|
||||||
|
|
||||||
# ---------- resolve_port ---------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class TestResolvePort:
|
|
||||||
def test_explicit_serial_passthrough(self) -> None:
|
|
||||||
assert connection.resolve_port("/dev/cu.usbmodem999") == "/dev/cu.usbmodem999"
|
|
||||||
|
|
||||||
def test_explicit_tcp_normalized(self) -> None:
|
|
||||||
assert connection.resolve_port("tcp://localhost") == "tcp://localhost:4403"
|
|
||||||
|
|
||||||
def test_no_port_no_devices_errors(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
||||||
monkeypatch.delenv("MESHTASTIC_MCP_TCP_HOST", raising=False)
|
|
||||||
with patch.object(devices, "list_devices", return_value=[]):
|
|
||||||
with pytest.raises(
|
|
||||||
connection.ConnectionError, match="No Meshtastic devices"
|
|
||||||
):
|
|
||||||
connection.resolve_port(None)
|
|
||||||
|
|
||||||
def test_no_port_one_candidate_selected(
|
|
||||||
self, monkeypatch: pytest.MonkeyPatch
|
|
||||||
) -> None:
|
|
||||||
monkeypatch.delenv("MESHTASTIC_MCP_TCP_HOST", raising=False)
|
|
||||||
fake = [{"port": "/dev/cu.usbmodem1", "likely_meshtastic": True}]
|
|
||||||
with patch.object(devices, "list_devices", return_value=fake):
|
|
||||||
assert connection.resolve_port(None) == "/dev/cu.usbmodem1"
|
|
||||||
|
|
||||||
def test_no_port_multiple_candidates_errors(
|
|
||||||
self, monkeypatch: pytest.MonkeyPatch
|
|
||||||
) -> None:
|
|
||||||
monkeypatch.delenv("MESHTASTIC_MCP_TCP_HOST", raising=False)
|
|
||||||
fake = [
|
|
||||||
{"port": "/dev/cu.usbmodem1", "likely_meshtastic": True},
|
|
||||||
{"port": "/dev/cu.usbmodem2", "likely_meshtastic": True},
|
|
||||||
]
|
|
||||||
with patch.object(devices, "list_devices", return_value=fake):
|
|
||||||
with pytest.raises(connection.ConnectionError, match="Multiple"):
|
|
||||||
connection.resolve_port(None)
|
|
||||||
|
|
||||||
def test_env_var_surfaces_tcp_via_devices(
|
|
||||||
self, monkeypatch: pytest.MonkeyPatch
|
|
||||||
) -> None:
|
|
||||||
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "localhost")
|
|
||||||
# Don't patch list_devices — let the real env-var path run, but stub
|
|
||||||
# the USB enumeration to keep the test hermetic.
|
|
||||||
with patch("meshtastic_mcp.devices.list_ports.comports", return_value=[]):
|
|
||||||
assert connection.resolve_port(None) == "tcp://localhost:4403"
|
|
||||||
|
|
||||||
|
|
||||||
# ---------- devices.list_devices TCP entry --------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class TestDevicesTcpEntry:
|
|
||||||
def test_no_env_var_no_tcp_entry(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
||||||
monkeypatch.delenv("MESHTASTIC_MCP_TCP_HOST", raising=False)
|
|
||||||
with patch("meshtastic_mcp.devices.list_ports.comports", return_value=[]):
|
|
||||||
ds = devices.list_devices()
|
|
||||||
assert all(not d["port"].startswith("tcp://") for d in ds)
|
|
||||||
|
|
||||||
def test_env_var_adds_tcp_entry(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
||||||
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "myhost:9999")
|
|
||||||
with patch("meshtastic_mcp.devices.list_ports.comports", return_value=[]):
|
|
||||||
ds = devices.list_devices()
|
|
||||||
tcp = [d for d in ds if d["port"].startswith("tcp://")]
|
|
||||||
assert len(tcp) == 1
|
|
||||||
assert tcp[0]["port"] == "tcp://myhost:9999"
|
|
||||||
assert tcp[0]["likely_meshtastic"] is True
|
|
||||||
assert tcp[0]["description"] == "meshtasticd (TCP)"
|
|
||||||
|
|
||||||
def test_tcp_entry_first_in_results(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
||||||
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "localhost")
|
|
||||||
with patch("meshtastic_mcp.devices.list_ports.comports", return_value=[]):
|
|
||||||
ds = devices.list_devices()
|
|
||||||
assert ds, "expected at least the TCP entry"
|
|
||||||
assert ds[0]["port"].startswith("tcp://")
|
|
||||||
|
|
||||||
def test_invalid_env_var_does_not_break_list_devices(
|
|
||||||
self, monkeypatch: pytest.MonkeyPatch
|
|
||||||
) -> None:
|
|
||||||
# `list_devices` is the diagnostic tool reached for when an env var
|
|
||||||
# isn't working — it must not throw on misconfiguration.
|
|
||||||
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "host:notaport")
|
|
||||||
with patch("meshtastic_mcp.devices.list_ports.comports", return_value=[]):
|
|
||||||
ds = devices.list_devices(include_unknown=True)
|
|
||||||
tcp = [d for d in ds if "TCP" in (d["description"] or "")]
|
|
||||||
assert len(tcp) == 1
|
|
||||||
assert tcp[0]["likely_meshtastic"] is False
|
|
||||||
assert "invalid MESHTASTIC_MCP_TCP_HOST" in tcp[0]["description"]
|
|
||||||
assert "not an integer" in tcp[0]["description"]
|
|
||||||
|
|
||||||
def test_invalid_env_var_excluded_from_resolve_port_autodetect(
|
|
||||||
self, monkeypatch: pytest.MonkeyPatch
|
|
||||||
) -> None:
|
|
||||||
# `likely_meshtastic=False` keeps the bad TCP entry out of the
|
|
||||||
# auto-select path — `resolve_port(None)` should still report
|
|
||||||
# "no Meshtastic devices" rather than picking a broken endpoint.
|
|
||||||
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "host:notaport")
|
|
||||||
with patch("meshtastic_mcp.devices.list_ports.comports", return_value=[]):
|
|
||||||
with pytest.raises(connection.ConnectionError, match="No Meshtastic"):
|
|
||||||
connection.resolve_port(None)
|
|
||||||
|
|
||||||
def test_invalid_env_var_does_not_double_tcp_scheme(
|
|
||||||
self, monkeypatch: pytest.MonkeyPatch
|
|
||||||
) -> None:
|
|
||||||
# If a user mistakenly sets `MESHTASTIC_MCP_TCP_HOST=tcp://host:bad`,
|
|
||||||
# the diagnostic entry must surface the raw value as-is rather than
|
|
||||||
# producing `tcp://tcp://host:bad`.
|
|
||||||
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "tcp://host:notaport")
|
|
||||||
with patch("meshtastic_mcp.devices.list_ports.comports", return_value=[]):
|
|
||||||
ds = devices.list_devices(include_unknown=True)
|
|
||||||
tcp = [d for d in ds if "TCP" in (d["description"] or "")]
|
|
||||||
assert len(tcp) == 1
|
|
||||||
assert tcp[0]["port"] == "tcp://host:notaport"
|
|
||||||
assert "tcp://tcp://" not in tcp[0]["port"]
|
|
||||||
|
|
||||||
def test_invalid_env_var_does_not_pre_empt_real_usb_devices(
|
|
||||||
self, monkeypatch: pytest.MonkeyPatch
|
|
||||||
) -> None:
|
|
||||||
# Sort ordering: a misconfigured TCP env var must NOT take position 0
|
|
||||||
# ahead of real USB candidates. Position 0 is reserved for the highest
|
|
||||||
# rank (likely_meshtastic=True), with TCP-before-USB as a tiebreaker
|
|
||||||
# within rank.
|
|
||||||
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "host:notaport")
|
|
||||||
|
|
||||||
# Stub a USB Meshtastic candidate (Espressif VID, port present in
|
|
||||||
# findPorts).
|
|
||||||
class FakeInfo:
|
|
||||||
def __init__(self, device: str, vid: int, pid: int) -> None:
|
|
||||||
self.device = device
|
|
||||||
self.vid = vid
|
|
||||||
self.pid = pid
|
|
||||||
self.description = "Heltec V3"
|
|
||||||
self.manufacturer = "Espressif"
|
|
||||||
self.product = "USB JTAG/serial"
|
|
||||||
self.serial_number = "abc"
|
|
||||||
|
|
||||||
fake_port = FakeInfo("/dev/cu.usbmodem4201", 0x303A, 0x1001)
|
|
||||||
with patch(
|
|
||||||
"meshtastic_mcp.devices.list_ports.comports", return_value=[fake_port]
|
|
||||||
), patch(
|
|
||||||
"meshtastic.util.findPorts",
|
|
||||||
return_value=["/dev/cu.usbmodem4201"],
|
|
||||||
):
|
|
||||||
ds = devices.list_devices(include_unknown=True)
|
|
||||||
|
|
||||||
assert ds, "expected at least the USB + TCP entries"
|
|
||||||
# Real USB candidate must be at position 0 — it's likely_meshtastic.
|
|
||||||
assert ds[0]["port"] == "/dev/cu.usbmodem4201"
|
|
||||||
assert ds[0]["likely_meshtastic"] is True
|
|
||||||
# The malformed TCP entry exists but lands among the unlikely entries.
|
|
||||||
tcp = [d for d in ds if "TCP" in (d["description"] or "")]
|
|
||||||
assert len(tcp) == 1
|
|
||||||
assert tcp[0]["likely_meshtastic"] is False
|
|
||||||
assert ds.index(tcp[0]) > 0
|
|
||||||
|
|
||||||
def test_likely_tcp_entry_wins_tiebreak_over_usb(
|
|
||||||
self, monkeypatch: pytest.MonkeyPatch
|
|
||||||
) -> None:
|
|
||||||
# Conversely, a *valid* TCP env var should sort ahead of USB
|
|
||||||
# candidates of equal likely_meshtastic rank — explicit env-var
|
|
||||||
# configuration is a precedence signal.
|
|
||||||
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "localhost:4403")
|
|
||||||
|
|
||||||
class FakeInfo:
|
|
||||||
def __init__(self, device: str, vid: int, pid: int) -> None:
|
|
||||||
self.device = device
|
|
||||||
self.vid = vid
|
|
||||||
self.pid = pid
|
|
||||||
self.description = "Heltec V3"
|
|
||||||
self.manufacturer = "Espressif"
|
|
||||||
self.product = "USB JTAG/serial"
|
|
||||||
self.serial_number = "abc"
|
|
||||||
|
|
||||||
fake_port = FakeInfo("/dev/cu.usbmodem4201", 0x303A, 0x1001)
|
|
||||||
with patch(
|
|
||||||
"meshtastic_mcp.devices.list_ports.comports", return_value=[fake_port]
|
|
||||||
), patch(
|
|
||||||
"meshtastic.util.findPorts",
|
|
||||||
return_value=["/dev/cu.usbmodem4201"],
|
|
||||||
):
|
|
||||||
ds = devices.list_devices()
|
|
||||||
|
|
||||||
assert ds[0]["port"] == "tcp://localhost:4403"
|
|
||||||
assert ds[0]["likely_meshtastic"] is True
|
|
||||||
|
|
||||||
|
|
||||||
# ---------- connect() routing ---------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class TestConnectRoutesTcp:
|
|
||||||
def test_connect_uses_tcp_interface_for_tcp_port(
|
|
||||||
self, monkeypatch: pytest.MonkeyPatch
|
|
||||||
) -> None:
|
|
||||||
"""Verify the TCP branch instantiates `TCPInterface(hostname, portNumber)`
|
|
||||||
and never touches `SerialInterface`."""
|
|
||||||
# Make sure the env var doesn't leak in and confuse resolve_port.
|
|
||||||
monkeypatch.delenv("MESHTASTIC_MCP_TCP_HOST", raising=False)
|
|
||||||
|
|
||||||
with patch("meshtastic.tcp_interface.TCPInterface") as mock_tcp, patch(
|
|
||||||
"meshtastic.serial_interface.SerialInterface"
|
|
||||||
) as mock_serial:
|
|
||||||
mock_tcp.return_value.close.return_value = None
|
|
||||||
with connection.connect(port="tcp://example.com:1234", timeout_s=12.0):
|
|
||||||
pass
|
|
||||||
|
|
||||||
mock_tcp.assert_called_once_with(
|
|
||||||
hostname="example.com",
|
|
||||||
portNumber=1234,
|
|
||||||
connectNow=True,
|
|
||||||
noProto=False,
|
|
||||||
timeout=12,
|
|
||||||
)
|
|
||||||
mock_serial.assert_not_called()
|
|
||||||
|
|
||||||
def test_connect_plumbs_timeout_to_serial_interface(
|
|
||||||
self, monkeypatch: pytest.MonkeyPatch
|
|
||||||
) -> None:
|
|
||||||
"""Verify the serial branch also propagates `timeout_s` so callers
|
|
||||||
passing a custom timeout to `device_info` / `list_nodes` / etc. don't
|
|
||||||
silently get the library default."""
|
|
||||||
monkeypatch.delenv("MESHTASTIC_MCP_TCP_HOST", raising=False)
|
|
||||||
|
|
||||||
with patch("meshtastic.serial_interface.SerialInterface") as mock_serial, patch(
|
|
||||||
"meshtastic.tcp_interface.TCPInterface"
|
|
||||||
) as mock_tcp:
|
|
||||||
mock_serial.return_value.close.return_value = None
|
|
||||||
with connection.connect(port="/dev/cu.fake", timeout_s=20.0):
|
|
||||||
pass
|
|
||||||
|
|
||||||
mock_serial.assert_called_once_with(
|
|
||||||
devPath="/dev/cu.fake",
|
|
||||||
connectNow=True,
|
|
||||||
noProto=False,
|
|
||||||
timeout=20,
|
|
||||||
)
|
|
||||||
mock_tcp.assert_not_called()
|
|
||||||
|
|
||||||
def test_connect_releases_lock_on_tcp_failure(
|
|
||||||
self, monkeypatch: pytest.MonkeyPatch
|
|
||||||
) -> None:
|
|
||||||
monkeypatch.delenv("MESHTASTIC_MCP_TCP_HOST", raising=False)
|
|
||||||
with patch("meshtastic.tcp_interface.TCPInterface") as mock_tcp:
|
|
||||||
mock_tcp.side_effect = RuntimeError("boom")
|
|
||||||
with pytest.raises(RuntimeError, match="boom"):
|
|
||||||
with connection.connect(port="tcp://locktest:4403"):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Lock should be released — a second connect attempt must not fail
|
|
||||||
# with "busy".
|
|
||||||
with patch("meshtastic.tcp_interface.TCPInterface") as mock_tcp:
|
|
||||||
mock_tcp.return_value.close.return_value = None
|
|
||||||
with connection.connect(port="tcp://locktest:4403"):
|
|
||||||
pass
|
|
||||||
+3
-3
@@ -120,7 +120,7 @@ lib_deps =
|
|||||||
[radiolib_base]
|
[radiolib_base]
|
||||||
lib_deps =
|
lib_deps =
|
||||||
# renovate: datasource=github-tags depName=RadioLib packageName=jgromes/RadioLib
|
# renovate: datasource=github-tags depName=RadioLib packageName=jgromes/RadioLib
|
||||||
https://github.com/jgromes/RadioLib/archive/afe72ae46a343e15e3cac7f26ac585c7f98bffe5.zip
|
https://github.com/jgromes/RadioLib/archive/refs/tags/7.6.0.zip
|
||||||
|
|
||||||
[device-ui_base]
|
[device-ui_base]
|
||||||
lib_deps =
|
lib_deps =
|
||||||
@@ -170,8 +170,8 @@ lib_deps =
|
|||||||
https://github.com/EmotiBit/EmotiBit_MLX90632/archive/refs/tags/v1.0.8.zip
|
https://github.com/EmotiBit/EmotiBit_MLX90632/archive/refs/tags/v1.0.8.zip
|
||||||
# renovate: datasource=github-tags depName=Adafruit MLX90614 packageName=adafruit/Adafruit_MLX90614
|
# renovate: datasource=github-tags depName=Adafruit MLX90614 packageName=adafruit/Adafruit_MLX90614
|
||||||
https://github.com/adafruit/Adafruit-MLX90614-Library/archive/refs/tags/2.1.6.zip
|
https://github.com/adafruit/Adafruit-MLX90614-Library/archive/refs/tags/2.1.6.zip
|
||||||
# renovate: datasource=github-tags depName=INA3221_RT packageName=RobTillaart/INA3221_RT
|
# renovate: datasource=git-refs depName=INA3221 packageName=https://github.com/sgtwilko/INA3221 gitBranch=FixOverflow
|
||||||
https://github.com/RobTillaart/INA3221_RT/archive/refs/tags/0.4.2.zip
|
https://github.com/sgtwilko/INA3221/archive/bb03d7e9bfcc74fc798838a54f4f99738f29fc6a.zip
|
||||||
# renovate: datasource=github-tags depName=QMC5883L Compass packageName=mprograms/QMC5883LCompass
|
# renovate: datasource=github-tags depName=QMC5883L Compass packageName=mprograms/QMC5883LCompass
|
||||||
https://github.com/mprograms/QMC5883LCompass/archive/refs/tags/v1.2.3.zip
|
https://github.com/mprograms/QMC5883LCompass/archive/refs/tags/v1.2.3.zip
|
||||||
# renovate: datasource=github-tags depName=DFRobot_RTU packageName=dfrobot/DFRobot_RTU
|
# renovate: datasource=github-tags depName=DFRobot_RTU packageName=dfrobot/DFRobot_RTU
|
||||||
|
|||||||
+1
-1
Submodule protobufs updated: 149586802f...1d6f1a71ff
+18
-15
@@ -26,8 +26,6 @@ SOFTWARE.*/
|
|||||||
|
|
||||||
#include "DebugConfiguration.h"
|
#include "DebugConfiguration.h"
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
#ifdef ARCH_PORTDUINO
|
#ifdef ARCH_PORTDUINO
|
||||||
#include "platform/portduino/PortduinoGlue.h"
|
#include "platform/portduino/PortduinoGlue.h"
|
||||||
#endif
|
#endif
|
||||||
@@ -121,22 +119,27 @@ bool Syslog::vlogf(uint16_t pri, const char *fmt, va_list args)
|
|||||||
|
|
||||||
bool Syslog::vlogf(uint16_t pri, const char *appName, const char *fmt, va_list args)
|
bool Syslog::vlogf(uint16_t pri, const char *appName, const char *fmt, va_list args)
|
||||||
{
|
{
|
||||||
// First measure the formatted length using a copy of args; passing args directly
|
char *message;
|
||||||
// to vsnprintf consumes it, and reusing a consumed va_list is undefined behavior.
|
size_t initialLen;
|
||||||
va_list args_measure;
|
size_t len;
|
||||||
va_copy(args_measure, args);
|
bool result;
|
||||||
int needed = vsnprintf(nullptr, 0, fmt, args_measure);
|
|
||||||
va_end(args_measure);
|
|
||||||
|
|
||||||
if (needed < 0)
|
initialLen = strlen(fmt);
|
||||||
return false; // encoding error
|
|
||||||
|
|
||||||
auto message = std::unique_ptr<char[]>(new char[static_cast<size_t>(needed) + 1]);
|
message = new char[initialLen + 1];
|
||||||
int written = vsnprintf(message.get(), static_cast<size_t>(needed) + 1, fmt, args);
|
|
||||||
if (written < 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return this->_sendLog(pri, appName, message.get());
|
len = vsnprintf(message, initialLen + 1, fmt, args);
|
||||||
|
if (len > initialLen) {
|
||||||
|
delete[] message;
|
||||||
|
message = new char[len + 1];
|
||||||
|
|
||||||
|
vsnprintf(message, len + 1, fmt, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = this->_sendLog(pri, appName, message);
|
||||||
|
|
||||||
|
delete[] message;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *message)
|
inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *message)
|
||||||
|
|||||||
+90
-60
@@ -79,46 +79,28 @@ bool copyFile(const char *from, const char *to)
|
|||||||
bool renameFile(const char *pathFrom, const char *pathTo)
|
bool renameFile(const char *pathFrom, const char *pathTo)
|
||||||
{
|
{
|
||||||
#ifdef FSCom
|
#ifdef FSCom
|
||||||
|
|
||||||
|
#ifdef ARCH_ESP32
|
||||||
|
// take SPI Lock
|
||||||
spiLock->lock();
|
spiLock->lock();
|
||||||
|
// rename was fixed for ESP32 IDF LittleFS in April
|
||||||
bool result = FSCom.rename(pathFrom, pathTo);
|
bool result = FSCom.rename(pathFrom, pathTo);
|
||||||
spiLock->unlock();
|
spiLock->unlock();
|
||||||
return result;
|
return result;
|
||||||
#else
|
#else
|
||||||
return false;
|
// copyFile does its own locking.
|
||||||
|
if (copyFile(pathFrom, pathTo) && FSCom.remove(pathFrom)) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Platform-agnostic filesystem format / wipe.
|
|
||||||
*
|
|
||||||
* On embedded targets (ESP32, NRF52, STM32WL, RP2040) this calls the
|
|
||||||
* native FSCom.format() which erases and reinitialises the LittleFS
|
|
||||||
* partition.
|
|
||||||
*
|
|
||||||
* On Portduino the fs::FS backend has no format() method. We instead
|
|
||||||
* delete /prefs (the only meshtastic data directory written at runtime)
|
|
||||||
* and return. rmDir("/prefs") is already called unconditionally by
|
|
||||||
* factoryReset() so this is a proven primitive on Portduino.
|
|
||||||
* FSBegin() is a no-op (#define FSBegin() true) on Portduino.
|
|
||||||
*
|
|
||||||
* @return true on success, false on failure or if no filesystem is configured.
|
|
||||||
*/
|
|
||||||
bool fsFormat()
|
|
||||||
{
|
|
||||||
#ifdef FSCom
|
|
||||||
#if defined(ARCH_PORTDUINO)
|
|
||||||
rmDir("/prefs");
|
|
||||||
return FSBegin();
|
|
||||||
#else
|
|
||||||
return FSCom.format();
|
|
||||||
#endif
|
|
||||||
#else
|
|
||||||
return false;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get the list of files in a directory.
|
* @brief Get the list of files in a directory.
|
||||||
*
|
*
|
||||||
@@ -141,21 +123,23 @@ std::vector<meshtastic_FileInfo> getFiles(const char *dirname, uint8_t levels)
|
|||||||
|
|
||||||
File file = root.openNextFile();
|
File file = root.openNextFile();
|
||||||
while (file) {
|
while (file) {
|
||||||
#ifdef ARCH_ESP32
|
|
||||||
const char *filepath = file.path();
|
|
||||||
#else
|
|
||||||
const char *filepath = file.name();
|
|
||||||
#endif
|
|
||||||
if (file.isDirectory() && !String(file.name()).endsWith(".")) {
|
if (file.isDirectory() && !String(file.name()).endsWith(".")) {
|
||||||
if (levels) {
|
if (levels) {
|
||||||
std::vector<meshtastic_FileInfo> subDirFilenames = getFiles(filepath, levels - 1);
|
#ifdef ARCH_ESP32
|
||||||
|
std::vector<meshtastic_FileInfo> subDirFilenames = getFiles(file.path(), levels - 1);
|
||||||
|
#else
|
||||||
|
std::vector<meshtastic_FileInfo> subDirFilenames = getFiles(file.name(), levels - 1);
|
||||||
|
#endif
|
||||||
filenames.insert(filenames.end(), subDirFilenames.begin(), subDirFilenames.end());
|
filenames.insert(filenames.end(), subDirFilenames.begin(), subDirFilenames.end());
|
||||||
file.close();
|
file.close();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
meshtastic_FileInfo fileInfo = {"", static_cast<uint32_t>(file.size())};
|
meshtastic_FileInfo fileInfo = {"", static_cast<uint32_t>(file.size())};
|
||||||
strncpy(fileInfo.file_name, filepath, sizeof(fileInfo.file_name) - 1);
|
#ifdef ARCH_ESP32
|
||||||
fileInfo.file_name[sizeof(fileInfo.file_name) - 1] = '\0';
|
strcpy(fileInfo.file_name, file.path());
|
||||||
|
#else
|
||||||
|
strcpy(fileInfo.file_name, file.name());
|
||||||
|
#endif
|
||||||
if (!String(fileInfo.file_name).endsWith(".")) {
|
if (!String(fileInfo.file_name).endsWith(".")) {
|
||||||
filenames.push_back(fileInfo);
|
filenames.push_back(fileInfo);
|
||||||
}
|
}
|
||||||
@@ -179,59 +163,98 @@ std::vector<meshtastic_FileInfo> getFiles(const char *dirname, uint8_t levels)
|
|||||||
void listDir(const char *dirname, uint8_t levels, bool del)
|
void listDir(const char *dirname, uint8_t levels, bool del)
|
||||||
{
|
{
|
||||||
#ifdef FSCom
|
#ifdef FSCom
|
||||||
|
#if (defined(ARCH_ESP32) || defined(ARCH_RP2040) || defined(ARCH_PORTDUINO))
|
||||||
char buffer[255];
|
char buffer[255];
|
||||||
|
#endif
|
||||||
File root = FSCom.open(dirname, FILE_O_READ);
|
File root = FSCom.open(dirname, FILE_O_READ);
|
||||||
if (!root || !root.isDirectory())
|
if (!root) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
if (!root.isDirectory()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
File file = root.openNextFile();
|
File file = root.openNextFile();
|
||||||
while (file && file.name()[0]) { // file.name()[0] check: workaround for Adafruit LittleFS nRF52 bug #4395
|
while (
|
||||||
#ifdef ARCH_ESP32
|
file &&
|
||||||
const char *filepath = file.path();
|
file.name()[0]) { // This file.name() check is a workaround for a bug in the Adafruit LittleFS nrf52 glue (see issue 4395)
|
||||||
#else
|
|
||||||
const char *filepath = file.name();
|
|
||||||
#endif
|
|
||||||
if (file.isDirectory() && !String(file.name()).endsWith(".")) {
|
if (file.isDirectory() && !String(file.name()).endsWith(".")) {
|
||||||
if (levels) {
|
if (levels) {
|
||||||
listDir(filepath, levels - 1, del);
|
#ifdef ARCH_ESP32
|
||||||
|
listDir(file.path(), levels - 1, del);
|
||||||
if (del) {
|
if (del) {
|
||||||
LOG_DEBUG("Remove %s", filepath);
|
LOG_DEBUG("Remove %s", file.path());
|
||||||
strncpy(buffer, filepath, sizeof(buffer) - 1);
|
strncpy(buffer, file.path(), sizeof(buffer));
|
||||||
buffer[sizeof(buffer) - 1] = '\0';
|
|
||||||
file.close();
|
file.close();
|
||||||
FSCom.rmdir(buffer);
|
FSCom.rmdir(buffer);
|
||||||
} else {
|
} else {
|
||||||
file.close();
|
file.close();
|
||||||
}
|
}
|
||||||
|
#elif (defined(ARCH_RP2040) || defined(ARCH_PORTDUINO))
|
||||||
|
listDir(file.name(), levels - 1, del);
|
||||||
|
if (del) {
|
||||||
|
LOG_DEBUG("Remove %s", file.name());
|
||||||
|
strncpy(buffer, file.name(), sizeof(buffer));
|
||||||
|
file.close();
|
||||||
|
FSCom.rmdir(buffer);
|
||||||
|
} else {
|
||||||
|
file.close();
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
LOG_DEBUG(" %s (directory)", file.name());
|
||||||
|
listDir(file.name(), levels - 1, del);
|
||||||
|
file.close();
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
#ifdef ARCH_ESP32
|
||||||
if (del) {
|
if (del) {
|
||||||
LOG_DEBUG("Delete %s", filepath);
|
LOG_DEBUG("Delete %s", file.path());
|
||||||
strncpy(buffer, filepath, sizeof(buffer) - 1);
|
strncpy(buffer, file.path(), sizeof(buffer));
|
||||||
buffer[sizeof(buffer) - 1] = '\0';
|
|
||||||
file.close();
|
file.close();
|
||||||
FSCom.remove(buffer);
|
FSCom.remove(buffer);
|
||||||
} else {
|
} else {
|
||||||
LOG_DEBUG(" %s (%i Bytes)", filepath, file.size());
|
LOG_DEBUG(" %s (%i Bytes)", file.path(), file.size());
|
||||||
file.close();
|
file.close();
|
||||||
}
|
}
|
||||||
|
#elif (defined(ARCH_RP2040) || defined(ARCH_PORTDUINO))
|
||||||
|
if (del) {
|
||||||
|
LOG_DEBUG("Delete %s", file.name());
|
||||||
|
strncpy(buffer, file.name(), sizeof(buffer));
|
||||||
|
file.close();
|
||||||
|
FSCom.remove(buffer);
|
||||||
|
} else {
|
||||||
|
LOG_DEBUG(" %s (%i Bytes)", file.name(), file.size());
|
||||||
|
file.close();
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
LOG_DEBUG(" %s (%i Bytes)", file.name(), file.size());
|
||||||
|
file.close();
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
file = root.openNextFile();
|
file = root.openNextFile();
|
||||||
}
|
}
|
||||||
#ifdef ARCH_ESP32
|
#ifdef ARCH_ESP32
|
||||||
const char *rootpath = root.path();
|
|
||||||
#else
|
|
||||||
const char *rootpath = root.name();
|
|
||||||
#endif
|
|
||||||
if (del) {
|
if (del) {
|
||||||
LOG_DEBUG("Remove %s", rootpath);
|
LOG_DEBUG("Remove %s", root.path());
|
||||||
strncpy(buffer, rootpath, sizeof(buffer) - 1);
|
strncpy(buffer, root.path(), sizeof(buffer));
|
||||||
buffer[sizeof(buffer) - 1] = '\0';
|
|
||||||
root.close();
|
root.close();
|
||||||
FSCom.rmdir(buffer);
|
FSCom.rmdir(buffer);
|
||||||
} else {
|
} else {
|
||||||
root.close();
|
root.close();
|
||||||
}
|
}
|
||||||
|
#elif (defined(ARCH_RP2040) || defined(ARCH_PORTDUINO))
|
||||||
|
if (del) {
|
||||||
|
LOG_DEBUG("Remove %s", root.name());
|
||||||
|
strncpy(buffer, root.name(), sizeof(buffer));
|
||||||
|
root.close();
|
||||||
|
FSCom.rmdir(buffer);
|
||||||
|
} else {
|
||||||
|
root.close();
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
root.close();
|
||||||
|
#endif
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +268,14 @@ void listDir(const char *dirname, uint8_t levels, bool del)
|
|||||||
void rmDir(const char *dirname)
|
void rmDir(const char *dirname)
|
||||||
{
|
{
|
||||||
#ifdef FSCom
|
#ifdef FSCom
|
||||||
|
|
||||||
|
#if (defined(ARCH_ESP32) || defined(ARCH_RP2040) || defined(ARCH_PORTDUINO))
|
||||||
listDir(dirname, 10, true);
|
listDir(dirname, 10, true);
|
||||||
|
#elif defined(ARCH_NRF52)
|
||||||
|
// nRF52 implementation of LittleFS has a recursive delete function
|
||||||
|
FSCom.rmdir_r(dirname);
|
||||||
|
#endif
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ void fsInit();
|
|||||||
void fsListFiles();
|
void fsListFiles();
|
||||||
bool copyFile(const char *from, const char *to);
|
bool copyFile(const char *from, const char *to);
|
||||||
bool renameFile(const char *pathFrom, const char *pathTo);
|
bool renameFile(const char *pathFrom, const char *pathTo);
|
||||||
bool fsFormat();
|
|
||||||
std::vector<meshtastic_FileInfo> getFiles(const char *dirname, uint8_t levels);
|
std::vector<meshtastic_FileInfo> getFiles(const char *dirname, uint8_t levels);
|
||||||
void listDir(const char *dirname, uint8_t levels, bool del = false);
|
void listDir(const char *dirname, uint8_t levels, bool del = false);
|
||||||
void rmDir(const char *dirname);
|
void rmDir(const char *dirname);
|
||||||
|
|||||||
+6
-3
@@ -53,7 +53,8 @@ class GPSStatus : public Status
|
|||||||
int32_t getLatitude() const
|
int32_t getLatitude() const
|
||||||
{
|
{
|
||||||
if (config.position.fixed_position) {
|
if (config.position.fixed_position) {
|
||||||
return localPosition.latitude_i;
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||||
|
return node->position.latitude_i;
|
||||||
} else {
|
} else {
|
||||||
return p.latitude_i;
|
return p.latitude_i;
|
||||||
}
|
}
|
||||||
@@ -62,7 +63,8 @@ class GPSStatus : public Status
|
|||||||
int32_t getLongitude() const
|
int32_t getLongitude() const
|
||||||
{
|
{
|
||||||
if (config.position.fixed_position) {
|
if (config.position.fixed_position) {
|
||||||
return localPosition.longitude_i;
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||||
|
return node->position.longitude_i;
|
||||||
} else {
|
} else {
|
||||||
return p.longitude_i;
|
return p.longitude_i;
|
||||||
}
|
}
|
||||||
@@ -71,7 +73,8 @@ class GPSStatus : public Status
|
|||||||
int32_t getAltitude() const
|
int32_t getAltitude() const
|
||||||
{
|
{
|
||||||
if (config.position.fixed_position) {
|
if (config.position.fixed_position) {
|
||||||
return localPosition.altitude;
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||||
|
return node->position.altitude;
|
||||||
} else {
|
} else {
|
||||||
return p.altitude;
|
return p.altitude;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ static File openFile(const char *filename, bool fullAtomic)
|
|||||||
{
|
{
|
||||||
concurrency::LockGuard g(spiLock);
|
concurrency::LockGuard g(spiLock);
|
||||||
LOG_DEBUG("Opening %s, fullAtomic=%d", filename, fullAtomic);
|
LOG_DEBUG("Opening %s, fullAtomic=%d", filename, fullAtomic);
|
||||||
|
#ifdef ARCH_NRF52
|
||||||
|
FSCom.remove(filename);
|
||||||
|
return FSCom.open(filename, FILE_O_WRITE);
|
||||||
|
#endif
|
||||||
if (!fullAtomic) {
|
if (!fullAtomic) {
|
||||||
FSCom.remove(filename); // Nuke the old file to make space (ignore if it !exists)
|
FSCom.remove(filename); // Nuke the old file to make space (ignore if it !exists)
|
||||||
}
|
}
|
||||||
@@ -63,6 +67,9 @@ bool SafeFile::close()
|
|||||||
f.close();
|
f.close();
|
||||||
spiLock->unlock();
|
spiLock->unlock();
|
||||||
|
|
||||||
|
#ifdef ARCH_NRF52
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
if (!testReadback())
|
if (!testReadback())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
|||||||
@@ -14,11 +14,6 @@ Lock::Lock() : handle(xSemaphoreCreateBinary())
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Lock::~Lock()
|
|
||||||
{
|
|
||||||
vSemaphoreDelete(handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Lock::lock()
|
void Lock::lock()
|
||||||
{
|
{
|
||||||
if (xSemaphoreTake(handle, portMAX_DELAY) == false) {
|
if (xSemaphoreTake(handle, portMAX_DELAY) == false) {
|
||||||
@@ -35,8 +30,6 @@ void Lock::unlock()
|
|||||||
#else
|
#else
|
||||||
Lock::Lock() {}
|
Lock::Lock() {}
|
||||||
|
|
||||||
Lock::~Lock() {}
|
|
||||||
|
|
||||||
void Lock::lock() {}
|
void Lock::lock() {}
|
||||||
|
|
||||||
void Lock::unlock() {}
|
void Lock::unlock() {}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ class Lock
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Lock();
|
Lock();
|
||||||
~Lock();
|
|
||||||
|
|
||||||
Lock(const Lock &) = delete;
|
Lock(const Lock &) = delete;
|
||||||
Lock &operator=(const Lock &) = delete;
|
Lock &operator=(const Lock &) = delete;
|
||||||
|
|||||||
+1
-1
@@ -80,7 +80,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
// Pre-hop drop handling (compile-time flag).
|
// Pre-hop drop handling (compile-time flag).
|
||||||
#ifndef MESHTASTIC_PREHOP_DROP
|
#ifndef MESHTASTIC_PREHOP_DROP
|
||||||
#define MESHTASTIC_PREHOP_DROP 1
|
#define MESHTASTIC_PREHOP_DROP 0
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/// Convert a preprocessor name into a quoted string
|
/// Convert a preprocessor name into a quoted string
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ enum LoRaRadioType {
|
|||||||
SX1280_RADIO,
|
SX1280_RADIO,
|
||||||
LR1110_RADIO,
|
LR1110_RADIO,
|
||||||
LR1120_RADIO,
|
LR1120_RADIO,
|
||||||
LR1121_RADIO,
|
LR1121_RADIO
|
||||||
LR2021_RADIO
|
|
||||||
};
|
};
|
||||||
|
|
||||||
extern LoRaRadioType radioType;
|
extern LoRaRadioType radioType;
|
||||||
+3
-11
@@ -1025,13 +1025,10 @@ void GPS::up()
|
|||||||
setPowerState(GPS_ACTIVE);
|
setPowerState(GPS_ACTIVE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We've finished a GPS search cycle (lock or timeout). Enter a low power state, potentially.
|
// We've got a GPS lock. Enter a low power state, potentially.
|
||||||
void GPS::down()
|
void GPS::down()
|
||||||
{
|
{
|
||||||
if (hasValidLocation)
|
scheduling.informGotLock();
|
||||||
scheduling.informGotLock();
|
|
||||||
else
|
|
||||||
scheduling.informSearchFailed();
|
|
||||||
uint32_t predictedSearchDuration = scheduling.predictedSearchDurationMs();
|
uint32_t predictedSearchDuration = scheduling.predictedSearchDurationMs();
|
||||||
uint32_t sleepTime = scheduling.msUntilNextSearch();
|
uint32_t sleepTime = scheduling.msUntilNextSearch();
|
||||||
uint32_t updateInterval = Default::getConfiguredOrDefaultMs(config.position.gps_update_interval);
|
uint32_t updateInterval = Default::getConfiguredOrDefaultMs(config.position.gps_update_interval);
|
||||||
@@ -1556,12 +1553,7 @@ std::unique_ptr<GPS> GPS::createGps()
|
|||||||
_en_gpio = PIN_GPS_EN;
|
_en_gpio = PIN_GPS_EN;
|
||||||
#endif
|
#endif
|
||||||
#ifdef ARCH_PORTDUINO
|
#ifdef ARCH_PORTDUINO
|
||||||
if (portduino_config.has_gps) {
|
if (!portduino_config.has_gps)
|
||||||
// These need to set as flags so later checks will pass on native and GPS will work.
|
|
||||||
// They are not used for any hardware access.
|
|
||||||
_rx_gpio = 1;
|
|
||||||
_tx_gpio = 1;
|
|
||||||
} else
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
#endif
|
#endif
|
||||||
if (!_rx_gpio || !_serial_gps) // Configured to have no GPS at all
|
if (!_rx_gpio || !_serial_gps) // Configured to have no GPS at all
|
||||||
|
|||||||
@@ -15,19 +15,6 @@ void GPSUpdateScheduling::informGotLock()
|
|||||||
searchEndedMs = millis();
|
searchEndedMs = millis();
|
||||||
LOG_DEBUG("Took %us to get lock", (searchEndedMs - searchStartedMs) / 1000);
|
LOG_DEBUG("Took %us to get lock", (searchEndedMs - searchStartedMs) / 1000);
|
||||||
updateLockTimePrediction();
|
updateLockTimePrediction();
|
||||||
consecutiveFailures = 0; // Drop back to fast cadence as soon as we acquire any fix
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search finished without obtaining a fix. We still need to mark the end time so
|
|
||||||
// the next sleep is timed correctly, but we must not feed the timeout duration
|
|
||||||
// into predictedMsToGetLock — doing so poisons msUntilNextSearch() and causes
|
|
||||||
// down() to fall into GPS_IDLE, leaving the chip awake on subsequent indoor cycles.
|
|
||||||
void GPSUpdateScheduling::informSearchFailed()
|
|
||||||
{
|
|
||||||
searchEndedMs = millis();
|
|
||||||
consecutiveFailures++;
|
|
||||||
LOG_DEBUG("GPS search ended without fix after %us (consecutive failures: %u)", (searchEndedMs - searchStartedMs) / 1000,
|
|
||||||
consecutiveFailures);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear old lock-time prediction data.
|
// Clear old lock-time prediction data.
|
||||||
@@ -38,7 +25,6 @@ void GPSUpdateScheduling::reset()
|
|||||||
searchEndedMs = 0;
|
searchEndedMs = 0;
|
||||||
searchCount = 0;
|
searchCount = 0;
|
||||||
predictedMsToGetLock = 0;
|
predictedMsToGetLock = 0;
|
||||||
consecutiveFailures = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// How many milliseconds before we should next search for GPS position
|
// How many milliseconds before we should next search for GPS position
|
||||||
@@ -50,20 +36,6 @@ uint32_t GPSUpdateScheduling::msUntilNextSearch()
|
|||||||
// Target interval (seconds), between GPS updates
|
// Target interval (seconds), between GPS updates
|
||||||
uint32_t updateInterval = Default::getConfiguredOrDefaultMs(config.position.gps_update_interval, default_gps_update_interval);
|
uint32_t updateInterval = Default::getConfiguredOrDefaultMs(config.position.gps_update_interval, default_gps_update_interval);
|
||||||
|
|
||||||
// After a failed search, back off: indoors / no-sky environments will keep failing,
|
|
||||||
// so wake at most once per broadcast interval rather than once per gps_update_interval.
|
|
||||||
// Capped at 1 hour so a user-configured very-long broadcast interval still retries
|
|
||||||
// periodically (in case conditions change). Reset on any successful lock.
|
|
||||||
if (consecutiveFailures > 0) {
|
|
||||||
constexpr uint32_t failureRetryCapMs = 60UL * 60UL * 1000UL; // 1 hour cap
|
|
||||||
uint32_t failureSleepMs =
|
|
||||||
Default::getConfiguredOrDefaultMs(config.position.position_broadcast_secs, default_broadcast_interval_secs);
|
|
||||||
if (failureSleepMs > failureRetryCapMs)
|
|
||||||
failureSleepMs = failureRetryCapMs;
|
|
||||||
if (updateInterval < failureSleepMs)
|
|
||||||
updateInterval = failureSleepMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check how long until we should start searching, to hopefully hit our target interval
|
// Check how long until we should start searching, to hopefully hit our target interval
|
||||||
uint32_t dueAtMs = searchEndedMs + updateInterval;
|
uint32_t dueAtMs = searchEndedMs + updateInterval;
|
||||||
uint32_t compensatedStart = dueAtMs - predictedMsToGetLock;
|
uint32_t compensatedStart = dueAtMs - predictedMsToGetLock;
|
||||||
@@ -99,18 +71,14 @@ bool GPSUpdateScheduling::isUpdateDue()
|
|||||||
bool GPSUpdateScheduling::searchedTooLong()
|
bool GPSUpdateScheduling::searchedTooLong()
|
||||||
{
|
{
|
||||||
constexpr uint32_t oneMinuteMs = 60UL * 1000UL;
|
constexpr uint32_t oneMinuteMs = 60UL * 1000UL;
|
||||||
constexpr uint32_t maxSearchClampMs = 15UL * oneMinuteMs; // Hard cap: 15 minutes is always too long
|
constexpr uint32_t maxSearchClampMs = 15UL * oneMinuteMs; // Hard cap: 15 minutes is always too long
|
||||||
constexpr uint32_t postFailureSearchMs = 5UL * oneMinuteMs; // Tighter dwell once we know the environment is hostile
|
|
||||||
uint32_t elapsed = elapsedSearchMs();
|
uint32_t elapsed = elapsedSearchMs();
|
||||||
|
|
||||||
// Anything over 15 minutes is too long, regardless of the broadcast interval.
|
// Anything over 15 minutes is too long, regardless of the broadcast interval.
|
||||||
|
// TODO: Make a smarter algorithm that backs off the search dwell time when not getting a lock.
|
||||||
if (elapsed > maxSearchClampMs)
|
if (elapsed > maxSearchClampMs)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
// After a prior failed search, shorten the dwell
|
|
||||||
if (consecutiveFailures > 0 && elapsed > postFailureSearchMs)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
uint32_t minimumOrConfiguredSecs =
|
uint32_t minimumOrConfiguredSecs =
|
||||||
Default::getConfiguredOrMinimumValue(config.position.position_broadcast_secs, default_broadcast_interval_secs);
|
Default::getConfiguredOrMinimumValue(config.position.position_broadcast_secs, default_broadcast_interval_secs);
|
||||||
uint32_t maxSearchMs = Default::getConfiguredOrDefaultMs(minimumOrConfiguredSecs, default_broadcast_interval_secs);
|
uint32_t maxSearchMs = Default::getConfiguredOrDefaultMs(minimumOrConfiguredSecs, default_broadcast_interval_secs);
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ class GPSUpdateScheduling
|
|||||||
public:
|
public:
|
||||||
// Marks the time of these events, for calculation use
|
// Marks the time of these events, for calculation use
|
||||||
void informSearching();
|
void informSearching();
|
||||||
void informGotLock(); // Predicted lock-time is recalculated here
|
void informGotLock(); // Predicted lock-time is recalculated here
|
||||||
void informSearchFailed(); // Search ended without a fix; prediction is left untouched
|
|
||||||
|
|
||||||
void reset(); // Reset the prediction - after GPS::disable() / GPS::enable()
|
void reset(); // Reset the prediction - after GPS::disable() / GPS::enable()
|
||||||
bool isUpdateDue(); // Is it time to begin searching for a GPS position?
|
bool isUpdateDue(); // Is it time to begin searching for a GPS position?
|
||||||
@@ -25,7 +24,6 @@ class GPSUpdateScheduling
|
|||||||
uint32_t searchEndedMs = 0;
|
uint32_t searchEndedMs = 0;
|
||||||
uint32_t searchCount = 0;
|
uint32_t searchCount = 0;
|
||||||
uint32_t predictedMsToGetLock = 0;
|
uint32_t predictedMsToGetLock = 0;
|
||||||
uint32_t consecutiveFailures = 0; // Count of search cycles that ended without a fix; reset on lock
|
|
||||||
|
|
||||||
const float weighting = 0.2; // Controls exponential smoothing of lock-times prediction. 20% weighting of "latest lock-time".
|
const float weighting = 0.2; // Controls exponential smoothing of lock-times prediction. 20% weighting of "latest lock-time".
|
||||||
};
|
};
|
||||||
@@ -1285,7 +1285,7 @@ void Screen::setFrames(FrameFocus focus)
|
|||||||
|
|
||||||
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||||
const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
|
const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
|
||||||
if (n && n->num != nodeDB->getNodeNum() && nodeInfoLiteIsFavorite(n)) {
|
if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) {
|
||||||
favoriteFrames.push_back(graphics::UIRenderer::drawFavoriteNode);
|
favoriteFrames.push_back(graphics::UIRenderer::drawFavoriteNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1670,7 +1670,7 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
|
|||||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from);
|
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from);
|
||||||
const meshtastic_Channel channel =
|
const meshtastic_Channel channel =
|
||||||
channels.getByIndex(packet->channel ? packet->channel : channels.getPrimaryIndex());
|
channels.getByIndex(packet->channel ? packet->channel : channels.getPrimaryIndex());
|
||||||
const char *longName = nodeInfoLiteHasUser(node) ? node->long_name : nullptr;
|
const char *longName = (node && node->has_user) ? node->user.long_name : nullptr;
|
||||||
|
|
||||||
const char *msgRaw = reinterpret_cast<const char *>(packet->decoded.payload.bytes);
|
const char *msgRaw = reinterpret_cast<const char *>(packet->decoded.payload.bytes);
|
||||||
|
|
||||||
|
|||||||
@@ -845,10 +845,10 @@ void menuHandler::messageViewModeMenu()
|
|||||||
// Encode peers
|
// Encode peers
|
||||||
for (size_t i = 0; i < uniquePeers.size(); ++i) {
|
for (size_t i = 0; i < uniquePeers.size(); ++i) {
|
||||||
uint32_t peer = uniquePeers[i];
|
uint32_t peer = uniquePeers[i];
|
||||||
const auto *node = nodeDB->getMeshNode(peer);
|
auto node = nodeDB->getMeshNode(peer);
|
||||||
std::string name;
|
std::string name;
|
||||||
if (nodeInfoLiteHasUser(node))
|
if (node && node->has_user)
|
||||||
name = sanitizeString(node->long_name).substr(0, 15);
|
name = sanitizeString(node->user.long_name).substr(0, 15);
|
||||||
else {
|
else {
|
||||||
char buf[20];
|
char buf[20];
|
||||||
snprintf(buf, sizeof(buf), "Node %08X", peer);
|
snprintf(buf, sizeof(buf), "Node %08X", peer);
|
||||||
@@ -1355,14 +1355,14 @@ void menuHandler::manageNodeMenu()
|
|||||||
static int optionsEnumArray[enumEnd] = {Back};
|
static int optionsEnumArray[enumEnd] = {Back};
|
||||||
int options = 1;
|
int options = 1;
|
||||||
|
|
||||||
if (nodeInfoLiteIsFavorite(node)) {
|
if (node->is_favorite) {
|
||||||
optionsArray[options] = "Unfavorite";
|
optionsArray[options] = "Unfavorite";
|
||||||
} else {
|
} else {
|
||||||
optionsArray[options] = "Favorite";
|
optionsArray[options] = "Favorite";
|
||||||
}
|
}
|
||||||
optionsEnumArray[options++] = Favorite;
|
optionsEnumArray[options++] = Favorite;
|
||||||
|
|
||||||
bool isMuted = nodeInfoLiteIsMuted(node);
|
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
||||||
if (isMuted) {
|
if (isMuted) {
|
||||||
optionsArray[options] = "Unmute Notifications";
|
optionsArray[options] = "Unmute Notifications";
|
||||||
} else {
|
} else {
|
||||||
@@ -1376,7 +1376,7 @@ void menuHandler::manageNodeMenu()
|
|||||||
optionsArray[options] = "Key Verification";
|
optionsArray[options] = "Key Verification";
|
||||||
optionsEnumArray[options++] = KeyVerification;
|
optionsEnumArray[options++] = KeyVerification;
|
||||||
|
|
||||||
if (nodeInfoLiteIsIgnored(node)) {
|
if (node->is_ignored) {
|
||||||
optionsArray[options] = "Unignore Node";
|
optionsArray[options] = "Unignore Node";
|
||||||
} else {
|
} else {
|
||||||
optionsArray[options] = "Ignore Node";
|
optionsArray[options] = "Ignore Node";
|
||||||
@@ -1386,8 +1386,8 @@ void menuHandler::manageNodeMenu()
|
|||||||
BannerOverlayOptions bannerOptions;
|
BannerOverlayOptions bannerOptions;
|
||||||
|
|
||||||
std::string title = "";
|
std::string title = "";
|
||||||
if (nodeInfoLiteHasUser(node) && node->long_name[0]) {
|
if (node->has_user && node->user.long_name && node->user.long_name[0]) {
|
||||||
title += sanitizeString(node->long_name).substr(0, 15);
|
title += sanitizeString(node->user.long_name).substr(0, 15);
|
||||||
} else {
|
} else {
|
||||||
char buf[20];
|
char buf[20];
|
||||||
snprintf(buf, sizeof(buf), "%08X", (unsigned int)node->num);
|
snprintf(buf, sizeof(buf), "%08X", (unsigned int)node->num);
|
||||||
@@ -1409,7 +1409,7 @@ void menuHandler::manageNodeMenu()
|
|||||||
if (!n) {
|
if (!n) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (nodeInfoLiteIsFavorite(n)) {
|
if (n->is_favorite) {
|
||||||
LOG_INFO("Removing node %08X from favorites", menuHandler::pickedNodeNum);
|
LOG_INFO("Removing node %08X from favorites", menuHandler::pickedNodeNum);
|
||||||
nodeDB->set_favorite(false, menuHandler::pickedNodeNum);
|
nodeDB->set_favorite(false, menuHandler::pickedNodeNum);
|
||||||
} else {
|
} else {
|
||||||
@@ -1426,9 +1426,13 @@ void menuHandler::manageNodeMenu()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool wasMuted = nodeInfoLiteIsMuted(n);
|
if (n->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) {
|
||||||
nodeInfoLiteSetBit(n, NODEINFO_BITFIELD_IS_MUTED_MASK, !wasMuted);
|
n->bitfield &= ~NODEINFO_BITFIELD_IS_MUTED_MASK;
|
||||||
LOG_INFO(wasMuted ? "Unmuted node %08X" : "Muted node %08X", menuHandler::pickedNodeNum);
|
LOG_INFO("Unmuted node %08X", menuHandler::pickedNodeNum);
|
||||||
|
} else {
|
||||||
|
n->bitfield |= NODEINFO_BITFIELD_IS_MUTED_MASK;
|
||||||
|
LOG_INFO("Muted node %08X", menuHandler::pickedNodeNum);
|
||||||
|
}
|
||||||
nodeDB->notifyObservers(true);
|
nodeDB->notifyObservers(true);
|
||||||
nodeDB->saveToDisk();
|
nodeDB->saveToDisk();
|
||||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
||||||
@@ -1457,11 +1461,11 @@ void menuHandler::manageNodeMenu()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nodeInfoLiteIsIgnored(n)) {
|
if (n->is_ignored) {
|
||||||
nodeInfoLiteSetBit(n, NODEINFO_BITFIELD_IS_IGNORED_MASK, false);
|
n->is_ignored = false;
|
||||||
LOG_INFO("Unignoring node %08X", menuHandler::pickedNodeNum);
|
LOG_INFO("Unignoring node %08X", menuHandler::pickedNodeNum);
|
||||||
} else {
|
} else {
|
||||||
nodeInfoLiteSetBit(n, NODEINFO_BITFIELD_IS_IGNORED_MASK, true);
|
n->is_ignored = true;
|
||||||
LOG_INFO("Ignoring node %08X", menuHandler::pickedNodeNum);
|
LOG_INFO("Ignoring node %08X", menuHandler::pickedNodeNum);
|
||||||
}
|
}
|
||||||
nodeDB->notifyObservers(true);
|
nodeDB->notifyObservers(true);
|
||||||
@@ -2075,9 +2079,9 @@ void menuHandler::removeFavoriteMenu()
|
|||||||
static const char *optionsArray[] = {"Back", "Yes"};
|
static const char *optionsArray[] = {"Back", "Yes"};
|
||||||
BannerOverlayOptions bannerOptions;
|
BannerOverlayOptions bannerOptions;
|
||||||
std::string message = "Unfavorite This Node?\n";
|
std::string message = "Unfavorite This Node?\n";
|
||||||
const auto *node = nodeDB->getMeshNode(graphics::UIRenderer::currentFavoriteNodeNum);
|
auto node = nodeDB->getMeshNode(graphics::UIRenderer::currentFavoriteNodeNum);
|
||||||
if (nodeInfoLiteHasUser(node)) {
|
if (node && node->has_user) {
|
||||||
message += sanitizeString(node->long_name).substr(0, 15);
|
message += sanitizeString(node->user.long_name).substr(0, 15);
|
||||||
}
|
}
|
||||||
bannerOptions.message = message.c_str();
|
bannerOptions.message = message.c_str();
|
||||||
bannerOptions.optionsArrayPtr = optionsArray;
|
bannerOptions.optionsArrayPtr = optionsArray;
|
||||||
|
|||||||
@@ -462,8 +462,8 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
}
|
}
|
||||||
case ThreadMode::DIRECT: {
|
case ThreadMode::DIRECT: {
|
||||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(currentPeer);
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(currentPeer);
|
||||||
if (nodeInfoLiteHasUser(node) && node->short_name[0]) {
|
if (node && node->has_user && node->user.short_name[0]) {
|
||||||
snprintf(titleStr, sizeof(titleStr), "@%s", node->short_name);
|
snprintf(titleStr, sizeof(titleStr), "@%s", node->user.short_name);
|
||||||
} else {
|
} else {
|
||||||
snprintf(titleStr, sizeof(titleStr), "@%08x", currentPeer);
|
snprintf(titleStr, sizeof(titleStr), "@%08x", currentPeer);
|
||||||
}
|
}
|
||||||
@@ -585,11 +585,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
meshtastic_NodeInfoLite *node_recipient = nodeDB->getMeshNode(m.dest);
|
meshtastic_NodeInfoLite *node_recipient = nodeDB->getMeshNode(m.dest);
|
||||||
|
|
||||||
char senderName[64] = "";
|
char senderName[64] = "";
|
||||||
if (nodeInfoLiteHasUser(node)) {
|
if (node && node->has_user) {
|
||||||
if (node->long_name[0]) {
|
if (node->user.long_name[0]) {
|
||||||
strncpy(senderName, node->long_name, sizeof(senderName) - 1);
|
strncpy(senderName, node->user.long_name, sizeof(senderName) - 1);
|
||||||
} else if (node->short_name[0]) {
|
} else if (node->user.short_name[0]) {
|
||||||
strncpy(senderName, node->short_name, sizeof(senderName) - 1);
|
strncpy(senderName, node->user.short_name, sizeof(senderName) - 1);
|
||||||
}
|
}
|
||||||
senderName[sizeof(senderName) - 1] = '\0';
|
senderName[sizeof(senderName) - 1] = '\0';
|
||||||
}
|
}
|
||||||
@@ -599,17 +599,18 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
|
|
||||||
// If this is *our own* message, override senderName to who the recipient was
|
// If this is *our own* message, override senderName to who the recipient was
|
||||||
bool mine = (m.sender == nodeDB->getNodeNum());
|
bool mine = (m.sender == nodeDB->getNodeNum());
|
||||||
if (mine && nodeInfoLiteHasUser(node_recipient)) {
|
if (mine && node_recipient && node_recipient->has_user) {
|
||||||
if (node_recipient->long_name[0]) {
|
if (node_recipient->user.long_name[0]) {
|
||||||
strncpy(senderName, node_recipient->long_name, sizeof(senderName) - 1);
|
strncpy(senderName, node_recipient->user.long_name, sizeof(senderName) - 1);
|
||||||
senderName[sizeof(senderName) - 1] = '\0';
|
senderName[sizeof(senderName) - 1] = '\0';
|
||||||
} else if (node_recipient->short_name[0]) {
|
} else if (node_recipient->user.short_name[0]) {
|
||||||
strncpy(senderName, node_recipient->short_name, sizeof(senderName) - 1);
|
strncpy(senderName, node_recipient->user.short_name, sizeof(senderName) - 1);
|
||||||
senderName[sizeof(senderName) - 1] = '\0';
|
senderName[sizeof(senderName) - 1] = '\0';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If recipient info is missing/empty, prefer a recipient identifier for outbound messages.
|
// If recipient info is missing/empty, prefer a recipient identifier for outbound messages.
|
||||||
if (mine && (!nodeInfoLiteHasUser(node_recipient) || (!node_recipient->long_name[0] && !node_recipient->short_name[0]))) {
|
if (mine && (!node_recipient || !node_recipient->has_user ||
|
||||||
|
(!node_recipient->user.long_name[0] && !node_recipient->user.short_name[0]))) {
|
||||||
snprintf(senderName, sizeof(senderName), "(%08x)", m.dest);
|
snprintf(senderName, sizeof(senderName), "(%08x)", m.dest);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1072,12 +1073,12 @@ void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const mesht
|
|||||||
// Banner logic
|
// Banner logic
|
||||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet.from);
|
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet.from);
|
||||||
char longName[64] = "?";
|
char longName[64] = "?";
|
||||||
if (nodeInfoLiteHasUser(node)) {
|
if (node && node->has_user) {
|
||||||
if (node->long_name[0]) {
|
if (node->user.long_name[0]) {
|
||||||
strncpy(longName, node->long_name, sizeof(longName) - 1);
|
strncpy(longName, node->user.long_name, sizeof(longName) - 1);
|
||||||
longName[sizeof(longName) - 1] = '\0';
|
longName[sizeof(longName) - 1] = '\0';
|
||||||
} else if (node->short_name[0]) {
|
} else if (node->user.short_name[0]) {
|
||||||
strncpy(longName, node->short_name, sizeof(longName) - 1);
|
strncpy(longName, node->user.short_name, sizeof(longName) - 1);
|
||||||
longName[sizeof(longName) - 1] = '\0';
|
longName[sizeof(longName) - 1] = '\0';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,23 +98,29 @@ std::string getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node,
|
|||||||
// 1) Choose target candidate (long vs short) only if present
|
// 1) Choose target candidate (long vs short) only if present
|
||||||
const char *raw = nullptr;
|
const char *raw = nullptr;
|
||||||
|
|
||||||
#if !MESHTASTIC_EXCLUDE_STATUS && !MESHTASTIC_EXCLUDE_STATUSDB
|
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||||
// If long-name mode is enabled, and we have a recent status for this node,
|
// If long-name mode is enabled, and we have a recent status for this node,
|
||||||
// prefer "(short_name) statusText" as the raw candidate. Pull straight out
|
// prefer "(short_name) statusText" as the raw candidate.
|
||||||
// of NodeDB's per-NodeNum cache instead of scanning a FIFO.
|
|
||||||
std::string composedFromStatus;
|
std::string composedFromStatus;
|
||||||
if (config.display.use_long_node_name && nodeInfoLiteHasUser(node) && nodeDB) {
|
if (config.display.use_long_node_name && node && node->has_user && statusMessageModule) {
|
||||||
meshtastic_StatusMessage cachedStatus;
|
const auto &recent = statusMessageModule->getRecentReceived();
|
||||||
if (nodeDB->copyNodeStatus(node->num, cachedStatus) && cachedStatus.status[0]) {
|
const StatusMessageModule::RecentStatus *found = nullptr;
|
||||||
const char *shortName = node->short_name;
|
for (auto it = recent.rbegin(); it != recent.rend(); ++it) {
|
||||||
const size_t statusLen = std::strlen(cachedStatus.status);
|
if (it->fromNodeId == node->num && !it->statusText.empty()) {
|
||||||
composedFromStatus.reserve(4 + (shortName ? std::strlen(shortName) : 0) + 1 + statusLen);
|
found = &(*it);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (found) {
|
||||||
|
const char *shortName = node->user.short_name;
|
||||||
|
composedFromStatus.reserve(4 + (shortName ? std::strlen(shortName) : 0) + 1 + found->statusText.size());
|
||||||
composedFromStatus += "(";
|
composedFromStatus += "(";
|
||||||
if (shortName && *shortName) {
|
if (shortName && *shortName) {
|
||||||
composedFromStatus += shortName;
|
composedFromStatus += shortName;
|
||||||
}
|
}
|
||||||
composedFromStatus += ") ";
|
composedFromStatus += ") ";
|
||||||
composedFromStatus += cachedStatus.status;
|
composedFromStatus += found->statusText;
|
||||||
|
|
||||||
raw = composedFromStatus.c_str(); // safe for now; we'll sanitize immediately into std::string
|
raw = composedFromStatus.c_str(); // safe for now; we'll sanitize immediately into std::string
|
||||||
}
|
}
|
||||||
@@ -123,8 +129,8 @@ std::string getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node,
|
|||||||
|
|
||||||
// If we didn't compose from status, use normal long/short selection
|
// If we didn't compose from status, use normal long/short selection
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
if (nodeInfoLiteHasUser(node)) {
|
if (node && node->has_user) {
|
||||||
raw = config.display.use_long_node_name ? node->long_name : node->short_name;
|
raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,7 +218,7 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries,
|
|||||||
static inline void applyFavoriteNodeNameColor(OLEDDisplay *display, const meshtastic_NodeInfoLite *node, const char *nodeName,
|
static inline void applyFavoriteNodeNameColor(OLEDDisplay *display, const meshtastic_NodeInfoLite *node, const char *nodeName,
|
||||||
int16_t nameX, int16_t y, int nameMaxWidth)
|
int16_t nameX, int16_t y, int nameMaxWidth)
|
||||||
{
|
{
|
||||||
if (!display || !node || !nodeInfoLiteIsFavorite(node) || !isTFTColoringEnabled() || !nodeName) {
|
if (!display || !node || !node->is_favorite || !isTFTColoringEnabled() || !nodeName) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,7 +259,7 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
|||||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
|
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
|
||||||
#endif
|
#endif
|
||||||
bool isMuted = nodeInfoLiteIsMuted(node);
|
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
||||||
|
|
||||||
char timeStr[10];
|
char timeStr[10];
|
||||||
uint32_t seconds = sinceLastSeen(node);
|
uint32_t seconds = sinceLastSeen(node);
|
||||||
@@ -273,14 +279,14 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
|||||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false);
|
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false);
|
||||||
if (nodeInfoLiteIsFavorite(node)) {
|
if (node->is_favorite) {
|
||||||
if (currentResolution == ScreenResolution::High) {
|
if (currentResolution == ScreenResolution::High) {
|
||||||
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||||
} else {
|
} else {
|
||||||
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (nodeInfoLiteIsIgnored(node) || isMuted) {
|
if (node->is_ignored || isMuted) {
|
||||||
if (currentResolution == ScreenResolution::High) {
|
if (currentResolution == ScreenResolution::High) {
|
||||||
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
|
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
|
||||||
} else {
|
} else {
|
||||||
@@ -315,20 +321,20 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
|||||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
|
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
|
||||||
#endif
|
#endif
|
||||||
bool isMuted = nodeInfoLiteIsMuted(node);
|
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
||||||
|
|
||||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
|
|
||||||
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false);
|
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false);
|
||||||
if (nodeInfoLiteIsFavorite(node)) {
|
if (node->is_favorite) {
|
||||||
if (currentResolution == ScreenResolution::High) {
|
if (currentResolution == ScreenResolution::High) {
|
||||||
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||||
} else {
|
} else {
|
||||||
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (nodeInfoLiteIsIgnored(node) || isMuted) {
|
if (node->is_ignored || isMuted) {
|
||||||
if (currentResolution == ScreenResolution::High) {
|
if (currentResolution == ScreenResolution::High) {
|
||||||
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
|
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
|
||||||
} else {
|
} else {
|
||||||
@@ -395,19 +401,15 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
|||||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
|
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
|
||||||
#endif
|
#endif
|
||||||
bool isMuted = nodeInfoLiteIsMuted(node);
|
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
||||||
char distStr[10] = "";
|
char distStr[10] = "";
|
||||||
|
|
||||||
const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||||
meshtastic_PositionLite ourPos;
|
if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) {
|
||||||
meshtastic_PositionLite theirPos;
|
double lat1 = ourNode->position.latitude_i * 1e-7;
|
||||||
const bool haveOurPos = ourNode && nodeDB->copyNodePosition(ourNode->num, ourPos);
|
double lon1 = ourNode->position.longitude_i * 1e-7;
|
||||||
const bool haveTheirPos = nodeDB->copyNodePosition(node->num, theirPos);
|
double lat2 = node->position.latitude_i * 1e-7;
|
||||||
if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node) && haveOurPos && haveTheirPos) {
|
double lon2 = node->position.longitude_i * 1e-7;
|
||||||
double lat1 = ourPos.latitude_i * 1e-7;
|
|
||||||
double lon1 = ourPos.longitude_i * 1e-7;
|
|
||||||
double lat2 = theirPos.latitude_i * 1e-7;
|
|
||||||
double lon2 = theirPos.longitude_i * 1e-7;
|
|
||||||
|
|
||||||
double earthRadiusKm = 6371.0;
|
double earthRadiusKm = 6371.0;
|
||||||
double dLat = (lat2 - lat1) * DEG_TO_RAD;
|
double dLat = (lat2 - lat1) * DEG_TO_RAD;
|
||||||
@@ -453,14 +455,14 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
|||||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false);
|
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false);
|
||||||
if (nodeInfoLiteIsFavorite(node)) {
|
if (node->is_favorite) {
|
||||||
if (currentResolution == ScreenResolution::High) {
|
if (currentResolution == ScreenResolution::High) {
|
||||||
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||||
} else {
|
} else {
|
||||||
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (nodeInfoLiteIsIgnored(node) || isMuted) {
|
if (node->is_ignored || isMuted) {
|
||||||
if (currentResolution == ScreenResolution::High) {
|
if (currentResolution == ScreenResolution::High) {
|
||||||
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
|
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
|
||||||
} else {
|
} else {
|
||||||
@@ -507,19 +509,19 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
|||||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||||
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
|
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
|
||||||
#endif
|
#endif
|
||||||
bool isMuted = nodeInfoLiteIsMuted(node);
|
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
||||||
|
|
||||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false);
|
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false);
|
||||||
if (nodeInfoLiteIsFavorite(node)) {
|
if (node->is_favorite) {
|
||||||
if (currentResolution == ScreenResolution::High) {
|
if (currentResolution == ScreenResolution::High) {
|
||||||
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||||
} else {
|
} else {
|
||||||
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (nodeInfoLiteIsIgnored(node) || isMuted) {
|
if (node->is_ignored || isMuted) {
|
||||||
if (currentResolution == ScreenResolution::High) {
|
if (currentResolution == ScreenResolution::High) {
|
||||||
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
|
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
|
||||||
} else {
|
} else {
|
||||||
@@ -540,11 +542,8 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
|||||||
int centerX = x + columnWidth - arrowXOffset;
|
int centerX = x + columnWidth - arrowXOffset;
|
||||||
int centerY = y + FONT_HEIGHT_SMALL / 2;
|
int centerY = y + FONT_HEIGHT_SMALL / 2;
|
||||||
|
|
||||||
meshtastic_PositionLite nodePos;
|
double nodeLat = node->position.latitude_i * 1e-7;
|
||||||
if (!nodeDB->copyNodePosition(node->num, nodePos))
|
double nodeLon = node->position.longitude_i * 1e-7;
|
||||||
return;
|
|
||||||
double nodeLat = nodePos.latitude_i * 1e-7;
|
|
||||||
double nodeLon = nodePos.longitude_i * 1e-7;
|
|
||||||
float bearing = GeoCoord::bearing(userLat, userLon, nodeLat, nodeLon);
|
float bearing = GeoCoord::bearing(userLat, userLon, nodeLat, nodeLon);
|
||||||
float relativeBearing = CompassRenderer::adjustBearingForCompassMode(bearing, myHeadingRadian);
|
float relativeBearing = CompassRenderer::adjustBearingForCompassMode(bearing, myHeadingRadian);
|
||||||
float relativeBearingDeg = CompassRenderer::radiansToDegrees360(relativeBearing);
|
float relativeBearingDeg = CompassRenderer::radiansToDegrees360(relativeBearing);
|
||||||
@@ -653,7 +652,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
|||||||
continue;
|
continue;
|
||||||
if (n->num == nodeDB->getNodeNum())
|
if (n->num == nodeDB->getNodeNum())
|
||||||
continue;
|
continue;
|
||||||
if (locationScreen && !nodeDB->hasNodePosition(n->num))
|
if (locationScreen && !n->has_position)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
drawList.push_back(n->num);
|
drawList.push_back(n->num);
|
||||||
@@ -884,15 +883,14 @@ void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
|||||||
void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
{
|
{
|
||||||
float headingRadian = 0.0f;
|
float headingRadian = 0.0f;
|
||||||
const auto *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
auto ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||||
meshtastic_PositionLite ourSelfPos;
|
if (!ourNode || !nodeDB->hasValidPosition(ourNode)) {
|
||||||
if (!ourNode || !nodeDB->hasValidPosition(ourNode) || !nodeDB->copyNodePosition(ourNode->num, ourSelfPos)) {
|
|
||||||
drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassUnknown, headingRadian, 0.0, 0.0);
|
drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassUnknown, headingRadian, 0.0, 0.0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
double lat = DegD(ourSelfPos.latitude_i);
|
double lat = DegD(ourNode->position.latitude_i);
|
||||||
double lon = DegD(ourSelfPos.longitude_i);
|
double lon = DegD(ourNode->position.longitude_i);
|
||||||
|
|
||||||
#if defined(M5STACK_UNITC6L)
|
#if defined(M5STACK_UNITC6L)
|
||||||
display->clear();
|
display->clear();
|
||||||
|
|||||||
@@ -317,12 +317,12 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
|
|||||||
for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) {
|
for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) {
|
||||||
char tempName[48] = {0};
|
char tempName[48] = {0};
|
||||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i + 1);
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i + 1);
|
||||||
if (nodeInfoLiteHasUser(node)) {
|
if (node && node->has_user) {
|
||||||
const char *rawName = nullptr;
|
const char *rawName = nullptr;
|
||||||
if (node->long_name[0]) {
|
if (node->user.long_name[0]) {
|
||||||
rawName = node->long_name;
|
rawName = node->user.long_name;
|
||||||
} else if (node->short_name[0]) {
|
} else if (node->user.short_name[0]) {
|
||||||
rawName = node->short_name;
|
rawName = node->user.short_name;
|
||||||
}
|
}
|
||||||
if (rawName) {
|
if (rawName) {
|
||||||
const int arrowWidth = (currentResolution == ScreenResolution::High)
|
const int arrowWidth = (currentResolution == ScreenResolution::High)
|
||||||
|
|||||||
@@ -464,11 +464,11 @@ static bool computeBottomCompassPlacement(OLEDDisplay *display, int16_t xOffset,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void drawTruncatedStatusLine(OLEDDisplay *display, int16_t x, int16_t y, const char *statusText)
|
static void drawTruncatedStatusLine(OLEDDisplay *display, int16_t x, int16_t y, const std::string &statusText)
|
||||||
{
|
{
|
||||||
// Fixed-buffer truncate helper replaces iterative std::string chopping to keep code size down.
|
// Fixed-buffer truncate helper replaces iterative std::string chopping to keep code size down.
|
||||||
char rawStatus[96];
|
char rawStatus[96];
|
||||||
snprintf(rawStatus, sizeof(rawStatus), " Status: %s", statusText ? statusText : "");
|
snprintf(rawStatus, sizeof(rawStatus), " Status: %s", statusText.c_str());
|
||||||
|
|
||||||
char clippedStatus[96];
|
char clippedStatus[96];
|
||||||
UIRenderer::truncateStringWithEmotes(display, rawStatus, clippedStatus, sizeof(clippedStatus), display->getWidth());
|
UIRenderer::truncateStringWithEmotes(display, rawStatus, clippedStatus, sizeof(clippedStatus), display->getWidth());
|
||||||
@@ -495,7 +495,7 @@ void graphics::UIRenderer::rebuildFavoritedNodes()
|
|||||||
meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
|
meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
|
||||||
if (!n || n->num == nodeDB->getNodeNum())
|
if (!n || n->num == nodeDB->getNodeNum())
|
||||||
continue;
|
continue;
|
||||||
if (nodeInfoLiteIsFavorite(n))
|
if (n->is_favorite)
|
||||||
favoritedNodes.push_back(n);
|
favoritedNodes.push_back(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -750,7 +750,7 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex];
|
meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex];
|
||||||
if (!node || node->num == nodeDB->getNodeNum() || !nodeInfoLiteIsFavorite(node))
|
if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite)
|
||||||
return;
|
return;
|
||||||
display->clear();
|
display->clear();
|
||||||
#if defined(M5STACK_UNITC6L)
|
#if defined(M5STACK_UNITC6L)
|
||||||
@@ -763,7 +763,7 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
|
|||||||
#endif
|
#endif
|
||||||
currentFavoriteNodeNum = node->num;
|
currentFavoriteNodeNum = node->num;
|
||||||
// === Create the shortName and title string ===
|
// === Create the shortName and title string ===
|
||||||
const char *shortName = (nodeInfoLiteHasUser(node) && node->short_name[0]) ? node->short_name : "Node";
|
const char *shortName = (node->has_user && node->user.short_name[0]) ? node->user.short_name : "Node";
|
||||||
char titlestr[40];
|
char titlestr[40];
|
||||||
snprintf(titlestr, sizeof(titlestr), "*%s*", shortName);
|
snprintf(titlestr, sizeof(titlestr), "*%s*", shortName);
|
||||||
|
|
||||||
@@ -781,9 +781,9 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
|
|||||||
// === 1. Long Name (always try to show first) ===
|
// === 1. Long Name (always try to show first) ===
|
||||||
const char *username;
|
const char *username;
|
||||||
if (currentResolution == ScreenResolution::UltraLow) {
|
if (currentResolution == ScreenResolution::UltraLow) {
|
||||||
username = (nodeInfoLiteHasUser(node) && node->long_name[0]) ? node->short_name : nullptr;
|
username = (node->has_user && node->user.long_name[0]) ? node->user.short_name : nullptr;
|
||||||
} else {
|
} else {
|
||||||
username = (nodeInfoLiteHasUser(node) && node->long_name[0]) ? node->long_name : nullptr;
|
username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print node's long name (e.g. "Backpack Node")
|
// Print node's long name (e.g. "Backpack Node")
|
||||||
@@ -796,14 +796,23 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
|
|||||||
UIRenderer::drawStringWithEmotes(display, x, getTextPositions(display)[line++], username, FONT_HEIGHT_SMALL, 1, false);
|
UIRenderer::drawStringWithEmotes(display, x, getTextPositions(display)[line++], username, FONT_HEIGHT_SMALL, 1, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !MESHTASTIC_EXCLUDE_STATUS && !MESHTASTIC_EXCLUDE_STATUSDB
|
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||||
// === Optional: Last received StatusMessage line for this node ===
|
// === Optional: Last received StatusMessage line for this node ===
|
||||||
// Display it directly under the username line (if we have one). The cache
|
// Display it directly under the username line (if we have one).
|
||||||
// lives on NodeDB now, keyed by NodeNum, so this is an O(1) lookup.
|
if (statusMessageModule) {
|
||||||
if (nodeDB) {
|
const auto &recent = statusMessageModule->getRecentReceived();
|
||||||
meshtastic_StatusMessage cachedStatus;
|
const StatusMessageModule::RecentStatus *found = nullptr;
|
||||||
if (nodeDB->copyNodeStatus(node->num, cachedStatus) && cachedStatus.status[0]) {
|
|
||||||
drawTruncatedStatusLine(display, x, getTextPositions(display)[line++], cachedStatus.status);
|
// Search newest-to-oldest
|
||||||
|
for (auto it = recent.rbegin(); it != recent.rend(); ++it) {
|
||||||
|
if (it->fromNodeId == node->num && !it->statusText.empty()) {
|
||||||
|
found = &(*it);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (found) {
|
||||||
|
drawTruncatedStatusLine(display, x, getTextPositions(display)[line++], found->statusText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -962,30 +971,25 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
|
|||||||
#if !defined(M5STACK_UNITC6L)
|
#if !defined(M5STACK_UNITC6L)
|
||||||
// === 4. Uptime (only show if metric is present) ===
|
// === 4. Uptime (only show if metric is present) ===
|
||||||
char uptimeStr[32] = "";
|
char uptimeStr[32] = "";
|
||||||
meshtastic_DeviceMetrics nodeMetrics;
|
if (node->has_device_metrics && node->device_metrics.has_uptime_seconds) {
|
||||||
const bool haveNodeMetrics = nodeDB->copyNodeTelemetry(node->num, nodeMetrics);
|
|
||||||
if (haveNodeMetrics && nodeMetrics.has_uptime_seconds) {
|
|
||||||
char upPrefix[12]; // enough for leftSideSpacing + "Up:"
|
char upPrefix[12]; // enough for leftSideSpacing + "Up:"
|
||||||
snprintf(upPrefix, sizeof(upPrefix), "%sUp:", leftSideSpacing);
|
snprintf(upPrefix, sizeof(upPrefix), "%sUp:", leftSideSpacing);
|
||||||
getUptimeStr(nodeMetrics.uptime_seconds * 1000, upPrefix, uptimeStr, sizeof(uptimeStr));
|
getUptimeStr(node->device_metrics.uptime_seconds * 1000, upPrefix, uptimeStr, sizeof(uptimeStr));
|
||||||
}
|
}
|
||||||
if (uptimeStr[0]) {
|
if (uptimeStr[0]) {
|
||||||
display->drawString(x, getTextPositions(display)[line++], uptimeStr);
|
display->drawString(x, getTextPositions(display)[line++], uptimeStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// === 5. Distance (only if both nodes have GPS position) ===
|
// === 5. Distance (only if both nodes have GPS position) ===
|
||||||
const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||||
char distStr[24] = ""; // Make buffer big enough for any string
|
char distStr[24] = ""; // Make buffer big enough for any string
|
||||||
bool haveDistance = false;
|
bool haveDistance = false;
|
||||||
|
|
||||||
meshtastic_PositionLite nodePos;
|
if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) {
|
||||||
meshtastic_PositionLite ourPos;
|
|
||||||
const bool haveNodePos = nodeDB->copyNodePosition(node->num, nodePos);
|
|
||||||
const bool haveOurPos = ourNode && nodeDB->copyNodePosition(ourNode->num, ourPos);
|
|
||||||
if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node) && haveNodePos && haveOurPos) {
|
|
||||||
// Use shared meter conversion, then format display units with lightweight integer rounding.
|
// Use shared meter conversion, then format display units with lightweight integer rounding.
|
||||||
const float distanceMeters = GeoCoord::latLongToMeter(DegD(nodePos.latitude_i), DegD(nodePos.longitude_i),
|
const float distanceMeters =
|
||||||
DegD(ourPos.latitude_i), DegD(ourPos.longitude_i));
|
GeoCoord::latLongToMeter(DegD(node->position.latitude_i), DegD(node->position.longitude_i),
|
||||||
|
DegD(ourNode->position.latitude_i), DegD(ourNode->position.longitude_i));
|
||||||
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
|
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
|
||||||
const int feet = static_cast<int>((distanceMeters * METERS_TO_FEET) + 0.5f);
|
const int feet = static_cast<int>((distanceMeters * METERS_TO_FEET) + 0.5f);
|
||||||
if (feet > 0 && feet < 1000) {
|
if (feet > 0 && feet < 1000) {
|
||||||
@@ -1020,19 +1024,19 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
|
|||||||
char batLine[32] = "";
|
char batLine[32] = "";
|
||||||
bool haveBatLine = false;
|
bool haveBatLine = false;
|
||||||
|
|
||||||
if (haveNodeMetrics) {
|
if (node->has_device_metrics) {
|
||||||
bool hasPct = nodeMetrics.has_battery_level;
|
bool hasPct = node->device_metrics.has_battery_level;
|
||||||
bool hasVolt = nodeMetrics.has_voltage && nodeMetrics.voltage > 0.001f;
|
bool hasVolt = node->device_metrics.has_voltage && node->device_metrics.voltage > 0.001f;
|
||||||
|
|
||||||
int pct = 0;
|
int pct = 0;
|
||||||
float volt = 0.0f;
|
float volt = 0.0f;
|
||||||
|
|
||||||
if (hasPct) {
|
if (hasPct) {
|
||||||
pct = (int)nodeMetrics.battery_level;
|
pct = (int)node->device_metrics.battery_level;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasVolt) {
|
if (hasVolt) {
|
||||||
volt = nodeMetrics.voltage;
|
volt = node->device_metrics.voltage;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasPct && pct > 0 && pct <= 100) {
|
if (hasPct && pct > 0 && pct <= 100) {
|
||||||
@@ -1072,11 +1076,11 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
|
|||||||
const bool hasNodePositionFix = nodeDB->hasValidPosition(node);
|
const bool hasNodePositionFix = nodeDB->hasValidPosition(node);
|
||||||
const char *statusLine1 = nullptr;
|
const char *statusLine1 = nullptr;
|
||||||
const char *statusLine2 = nullptr;
|
const char *statusLine2 = nullptr;
|
||||||
if (hasOwnPositionFix && hasNodePositionFix && haveOurPos && haveNodePos) {
|
if (hasOwnPositionFix && hasNodePositionFix) {
|
||||||
const auto &op = ourPos;
|
const auto &op = ourNode->position;
|
||||||
showCompass = CompassRenderer::getHeadingRadians(DegD(op.latitude_i), DegD(op.longitude_i), myHeading);
|
showCompass = CompassRenderer::getHeadingRadians(DegD(op.latitude_i), DegD(op.longitude_i), myHeading);
|
||||||
if (showCompass) {
|
if (showCompass) {
|
||||||
const auto &p = nodePos;
|
const auto &p = node->position;
|
||||||
bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i));
|
bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i));
|
||||||
bearing = CompassRenderer::adjustBearingForCompassMode(bearing, myHeading);
|
bearing = CompassRenderer::adjustBearingForCompassMode(bearing, myHeading);
|
||||||
} else {
|
} else {
|
||||||
@@ -1126,7 +1130,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
|
|||||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
int line = 1;
|
int line = 1;
|
||||||
const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||||
|
|
||||||
// === Header ===
|
// === Header ===
|
||||||
if (currentResolution == ScreenResolution::UltraLow) {
|
if (currentResolution == ScreenResolution::UltraLow) {
|
||||||
@@ -1266,7 +1270,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
|
|||||||
int textWidth = 0;
|
int textWidth = 0;
|
||||||
int nameX = 0;
|
int nameX = 0;
|
||||||
int yOffset = (currentResolution == ScreenResolution::High) ? 0 : 5;
|
int yOffset = (currentResolution == ScreenResolution::High) ? 0 : 5;
|
||||||
const char *longName = (nodeInfoLiteHasUser(ourNode) && ourNode->long_name[0]) ? ourNode->long_name : "";
|
const char *longName = (ourNode && ourNode->has_user && ourNode->user.long_name[0]) ? ourNode->user.long_name : "";
|
||||||
const char *shortName = owner.short_name ? owner.short_name : "";
|
const char *shortName = owner.short_name ? owner.short_name : "";
|
||||||
char combinedName[96];
|
char combinedName[96];
|
||||||
if (longName[0] && shortName[0]) {
|
if (longName[0] && shortName[0]) {
|
||||||
@@ -1593,7 +1597,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
|||||||
geoCoord.updateCoords(int32_t(gpsStatus->getLatitude()), int32_t(gpsStatus->getLongitude()),
|
geoCoord.updateCoords(int32_t(gpsStatus->getLatitude()), int32_t(gpsStatus->getLongitude()),
|
||||||
int32_t(gpsStatus->getAltitude()));
|
int32_t(gpsStatus->getAltitude()));
|
||||||
|
|
||||||
const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||||
const bool hasOwnPositionFix = (ourNode && nodeDB->hasValidPosition(ourNode));
|
const bool hasOwnPositionFix = (ourNode && nodeDB->hasValidPosition(ourNode));
|
||||||
const bool hasLiveGpsFix =
|
const bool hasLiveGpsFix =
|
||||||
(gpsStatus && gpsStatus->getHasLock() && (gpsStatus->getLatitude() != 0 || gpsStatus->getLongitude() != 0));
|
(gpsStatus && gpsStatus->getHasLock() && (gpsStatus->getLatitude() != 0 || gpsStatus->getLongitude() != 0));
|
||||||
@@ -1609,11 +1613,9 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
|||||||
headingLat = DegD(gpsStatus->getLatitude());
|
headingLat = DegD(gpsStatus->getLatitude());
|
||||||
headingLon = DegD(gpsStatus->getLongitude());
|
headingLon = DegD(gpsStatus->getLongitude());
|
||||||
} else if (hasOwnPositionFix) {
|
} else if (hasOwnPositionFix) {
|
||||||
meshtastic_PositionLite ownPos;
|
const auto &op = ourNode->position;
|
||||||
if (nodeDB->copyNodePosition(ourNode->num, ownPos)) {
|
headingLat = DegD(op.latitude_i);
|
||||||
headingLat = DegD(ownPos.latitude_i);
|
headingLon = DegD(op.longitude_i);
|
||||||
headingLon = DegD(ownPos.longitude_i);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
validHeading = CompassRenderer::getHeadingRadians(headingLat, headingLon, heading);
|
validHeading = CompassRenderer::getHeadingRadians(headingLat, headingLon, heading);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -362,8 +362,8 @@ std::string InkHUD::Applet::parseShortName(meshtastic_NodeInfoLite *node)
|
|||||||
assert(node);
|
assert(node);
|
||||||
|
|
||||||
// Use the true shortname if known, and doesn't contain any unprintable characters (emoji, etc.)
|
// Use the true shortname if known, and doesn't contain any unprintable characters (emoji, etc.)
|
||||||
if (nodeInfoLiteHasUser(node)) {
|
if (node->has_user) {
|
||||||
std::string parsed = parse(node->short_name);
|
std::string parsed = parse(node->user.short_name);
|
||||||
if (isPrintable(parsed))
|
if (isPrintable(parsed))
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,10 +136,9 @@ void InkHUD::MapApplet::onRender(bool full)
|
|||||||
printAt(vertBarX + (bottomLabelW / 2) + 1, bottomLabelY + (bottomLabelH / 2), vertBottomLabel, CENTER, MIDDLE);
|
printAt(vertBarX + (bottomLabelW / 2) + 1, bottomLabelY + (bottomLabelH / 2), vertBottomLabel, CENTER, MIDDLE);
|
||||||
|
|
||||||
// Draw our node LAST with full white fill + outline
|
// Draw our node LAST with full white fill + outline
|
||||||
const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||||
meshtastic_PositionLite ourSelfPos;
|
if (ourNode && nodeDB->hasValidPosition(ourNode)) {
|
||||||
if (ourNode && nodeDB->hasValidPosition(ourNode) && nodeDB->copyNodePosition(ourNode->num, ourSelfPos)) {
|
Marker self = calculateMarker(ourNode->position.latitude_i * 1e-7, ourNode->position.longitude_i * 1e-7, false, 0);
|
||||||
Marker self = calculateMarker(ourSelfPos.latitude_i * 1e-7, ourSelfPos.longitude_i * 1e-7, false, 0);
|
|
||||||
|
|
||||||
int16_t centerX = X(0.5) + (self.eastMeters * metersToPx);
|
int16_t centerX = X(0.5) + (self.eastMeters * metersToPx);
|
||||||
int16_t centerY = Y(0.5) - (self.northMeters * metersToPx);
|
int16_t centerY = Y(0.5) - (self.northMeters * metersToPx);
|
||||||
@@ -169,11 +168,10 @@ void InkHUD::MapApplet::onRender(bool full)
|
|||||||
void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
|
void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
|
||||||
{
|
{
|
||||||
// If we have a valid position for our own node, use that as the anchor
|
// If we have a valid position for our own node, use that as the anchor
|
||||||
const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||||
meshtastic_PositionLite ourSelfPos;
|
if (ourNode && nodeDB->hasValidPosition(ourNode)) {
|
||||||
if (ourNode && nodeDB->hasValidPosition(ourNode) && nodeDB->copyNodePosition(ourNode->num, ourSelfPos)) {
|
*lat = ourNode->position.latitude_i * 1e-7;
|
||||||
*lat = ourSelfPos.latitude_i * 1e-7;
|
*lng = ourNode->position.longitude_i * 1e-7;
|
||||||
*lng = ourSelfPos.longitude_i * 1e-7;
|
|
||||||
} else {
|
} else {
|
||||||
// Find mean lat long coords
|
// Find mean lat long coords
|
||||||
// ============================
|
// ============================
|
||||||
@@ -203,13 +201,9 @@ void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
|
|||||||
if (!shouldDrawNode(node))
|
if (!shouldDrawNode(node))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
meshtastic_PositionLite pos;
|
|
||||||
if (!nodeDB->copyNodePosition(node->num, pos))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Latitude and Longitude of node, in radians
|
// Latitude and Longitude of node, in radians
|
||||||
float latRad = pos.latitude_i * (1e-7) * DEG_TO_RAD;
|
float latRad = node->position.latitude_i * (1e-7) * DEG_TO_RAD;
|
||||||
float lngRad = pos.longitude_i * (1e-7) * DEG_TO_RAD;
|
float lngRad = node->position.longitude_i * (1e-7) * DEG_TO_RAD;
|
||||||
|
|
||||||
// Convert to cartesian points, with center of earth at 0, 0, 0
|
// Convert to cartesian points, with center of earth at 0, 0, 0
|
||||||
// Exact distance from center is irrelevant, as we're only interested in the vector
|
// Exact distance from center is irrelevant, as we're only interested in the vector
|
||||||
@@ -306,17 +300,13 @@ void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
|
|||||||
if (!shouldDrawNode(node))
|
if (!shouldDrawNode(node))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
meshtastic_PositionLite pos;
|
|
||||||
if (!nodeDB->copyNodePosition(node->num, pos))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Check for a new top or bottom latitude
|
// Check for a new top or bottom latitude
|
||||||
float latNode = pos.latitude_i * 1e-7;
|
float latNode = node->position.latitude_i * 1e-7;
|
||||||
northernmost = max(northernmost, latNode);
|
northernmost = max(northernmost, latNode);
|
||||||
southernmost = min(southernmost, latNode);
|
southernmost = min(southernmost, latNode);
|
||||||
|
|
||||||
// Longitude is trickier
|
// Longitude is trickier
|
||||||
float lngNode = pos.longitude_i * 1e-7;
|
float lngNode = node->position.longitude_i * 1e-7;
|
||||||
float degEastward = fmod(((lngNode - lngCenter) + 360), 360); // Degrees traveled east from lngCenter to reach node
|
float degEastward = fmod(((lngNode - lngCenter) + 360), 360); // Degrees traveled east from lngCenter to reach node
|
||||||
float degWestward = abs(fmod(((lngNode - lngCenter) - 360), 360)); // Degrees traveled west from lngCenter to reach node
|
float degWestward = abs(fmod(((lngNode - lngCenter) - 360), 360)); // Degrees traveled west from lngCenter to reach node
|
||||||
if (degEastward < degWestward)
|
if (degEastward < degWestward)
|
||||||
@@ -382,13 +372,10 @@ void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node)
|
|||||||
{
|
{
|
||||||
// Find x and y position based on node's position in nodeDB
|
// Find x and y position based on node's position in nodeDB
|
||||||
assert(nodeDB->hasValidPosition(node));
|
assert(nodeDB->hasValidPosition(node));
|
||||||
meshtastic_PositionLite pos;
|
Marker m = calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style
|
||||||
const bool hasPos = nodeDB->copyNodePosition(node->num, pos);
|
node->position.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style
|
||||||
assert(hasPos);
|
node->has_hops_away, // Is the hopsAway number valid
|
||||||
Marker m = calculateMarker(pos.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style
|
node->hops_away // Hops away
|
||||||
pos.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style
|
|
||||||
node->has_hops_away, // Is the hopsAway number valid
|
|
||||||
node->hops_away // Hops away
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Convert to pixel coords
|
// Convert to pixel coords
|
||||||
@@ -529,16 +516,13 @@ void InkHUD::MapApplet::calculateAllMarkers()
|
|||||||
if (node->num == nodeDB->getNodeNum())
|
if (node->num == nodeDB->getNodeNum())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
meshtastic_PositionLite pos;
|
|
||||||
if (!nodeDB->copyNodePosition(node->num, pos))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Calculate marker and store it
|
// Calculate marker and store it
|
||||||
markers.push_back(calculateMarker(pos.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style
|
markers.push_back(
|
||||||
pos.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style
|
calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style
|
||||||
node->has_hops_away, // Is the hopsAway number valid
|
node->position.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style
|
||||||
node->hops_away // Hops away
|
node->has_hops_away, // Is the hopsAway number valid
|
||||||
));
|
node->hops_away // Hops away
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,23 +50,21 @@ ProcessMessage InkHUD::NodeListApplet::handleReceived(const meshtastic_MeshPacke
|
|||||||
c.signal = getSignalStrength(mp.rx_snr, mp.rx_rssi);
|
c.signal = getSignalStrength(mp.rx_snr, mp.rx_rssi);
|
||||||
|
|
||||||
// Assemble info: from nodeDB (needed to detect changes)
|
// Assemble info: from nodeDB (needed to detect changes)
|
||||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(c.nodeNum);
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(c.nodeNum);
|
||||||
const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||||
if (node) {
|
if (node) {
|
||||||
if (node->has_hops_away)
|
if (node->has_hops_away)
|
||||||
c.hopsAway = node->hops_away;
|
c.hopsAway = node->hops_away;
|
||||||
|
|
||||||
if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) {
|
if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) {
|
||||||
meshtastic_PositionLite ourPos;
|
// Get lat and long as float
|
||||||
meshtastic_PositionLite theirPos;
|
// Meshtastic stores these as integers internally
|
||||||
if (nodeDB->copyNodePosition(ourNode->num, ourPos) && nodeDB->copyNodePosition(node->num, theirPos)) {
|
float ourLat = ourNode->position.latitude_i * 1e-7;
|
||||||
float ourLat = ourPos.latitude_i * 1e-7;
|
float ourLong = ourNode->position.longitude_i * 1e-7;
|
||||||
float ourLong = ourPos.longitude_i * 1e-7;
|
float theirLat = node->position.latitude_i * 1e-7;
|
||||||
float theirLat = theirPos.latitude_i * 1e-7;
|
float theirLong = node->position.longitude_i * 1e-7;
|
||||||
float theirLong = theirPos.longitude_i * 1e-7;
|
|
||||||
|
|
||||||
c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong);
|
c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,8 +179,8 @@ void InkHUD::NodeListApplet::onRender(bool full)
|
|||||||
// -- Longname --
|
// -- Longname --
|
||||||
// Parse special chars in long name
|
// Parse special chars in long name
|
||||||
// Use node id if unknown
|
// Use node id if unknown
|
||||||
if (nodeInfoLiteHasUser(node))
|
if (node && node->has_user)
|
||||||
longName = parse(node->long_name); // Found in nodeDB
|
longName = parse(node->user.long_name); // Found in nodeDB
|
||||||
else {
|
else {
|
||||||
// Not found in nodeDB, show a hex nodeid instead
|
// Not found in nodeDB, show a hex nodeid instead
|
||||||
longName = hexifyNodeNum(nodeNum);
|
longName = hexifyNodeNum(nodeNum);
|
||||||
|
|||||||
@@ -2090,7 +2090,7 @@ void InkHUD::MenuApplet::populateRecipientPage()
|
|||||||
|
|
||||||
// Count favorites
|
// Count favorites
|
||||||
for (uint32_t i = 0; i < nodeCount; i++) {
|
for (uint32_t i = 0; i < nodeCount; i++) {
|
||||||
if (nodeInfoLiteIsFavorite(nodeDB->getMeshNodeByIndex(i)))
|
if (nodeDB->getMeshNodeByIndex(i)->is_favorite)
|
||||||
favoriteCount++;
|
favoriteCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2098,10 +2098,10 @@ void InkHUD::MenuApplet::populateRecipientPage()
|
|||||||
// Don't want some monstrous list that takes 100 clicks to reach exit
|
// Don't want some monstrous list that takes 100 clicks to reach exit
|
||||||
if (favoriteCount < 20) {
|
if (favoriteCount < 20) {
|
||||||
for (uint32_t i = 0; i < nodeCount; i++) {
|
for (uint32_t i = 0; i < nodeCount; i++) {
|
||||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||||
|
|
||||||
// Skip node if not a favorite
|
// Skip node if not a favorite
|
||||||
if (!nodeInfoLiteIsFavorite(node))
|
if (!node->is_favorite)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
CannedMessages::RecipientItem r;
|
CannedMessages::RecipientItem r;
|
||||||
@@ -2111,8 +2111,8 @@ void InkHUD::MenuApplet::populateRecipientPage()
|
|||||||
|
|
||||||
// Set a label for the menu item
|
// Set a label for the menu item
|
||||||
r.label = "DM: ";
|
r.label = "DM: ";
|
||||||
if (nodeInfoLiteHasUser(node))
|
if (node->has_user)
|
||||||
r.label += parse(node->long_name);
|
r.label += parse(node->user.long_name);
|
||||||
else
|
else
|
||||||
r.label += hexifyNodeNum(node->num); // Unsure if it's possible to favorite a node without NodeInfo?
|
r.label += hexifyNodeNum(node->num); // Unsure if it's possible to favorite a node without NodeInfo?
|
||||||
|
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila
|
|||||||
text += msgIsBroadcast ? "From:" : "DM: ";
|
text += msgIsBroadcast ? "From:" : "DM: ";
|
||||||
|
|
||||||
// Sender id
|
// Sender id
|
||||||
if (nodeInfoLiteHasUser(node))
|
if (node && node->has_user)
|
||||||
text += parseShortName(node);
|
text += parseShortName(node);
|
||||||
else
|
else
|
||||||
text += hexifyNodeNum(message->sender);
|
text += hexifyNodeNum(message->sender);
|
||||||
@@ -255,7 +255,7 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila
|
|||||||
text += msgIsBroadcast ? "Msg from " : "DM from ";
|
text += msgIsBroadcast ? "Msg from " : "DM from ";
|
||||||
|
|
||||||
// Sender id
|
// Sender id
|
||||||
if (nodeInfoLiteHasUser(node))
|
if (node && node->has_user)
|
||||||
text += parseShortName(node);
|
text += parseShortName(node);
|
||||||
else
|
else
|
||||||
text += hexifyNodeNum(message->sender);
|
text += hexifyNodeNum(message->sender);
|
||||||
|
|||||||
@@ -70,10 +70,10 @@ void InkHUD::AllMessageApplet::onRender(bool full)
|
|||||||
// - short name and long name, if available, or
|
// - short name and long name, if available, or
|
||||||
// - node id
|
// - node id
|
||||||
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(message->sender);
|
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(message->sender);
|
||||||
if (nodeInfoLiteHasUser(sender)) {
|
if (sender && sender->has_user) {
|
||||||
header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc)
|
header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc)
|
||||||
header += " (";
|
header += " (";
|
||||||
header += parse(sender->long_name);
|
header += parse(sender->user.long_name);
|
||||||
header += ")";
|
header += ")";
|
||||||
} else
|
} else
|
||||||
header += hexifyNodeNum(message->sender);
|
header += hexifyNodeNum(message->sender);
|
||||||
|
|||||||
@@ -66,10 +66,10 @@ void InkHUD::DMApplet::onRender(bool full)
|
|||||||
// - shortname and long name, if available, or
|
// - shortname and long name, if available, or
|
||||||
// - node id
|
// - node id
|
||||||
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(latestMessage->dm.sender);
|
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(latestMessage->dm.sender);
|
||||||
if (nodeInfoLiteHasUser(sender)) {
|
if (sender && sender->has_user) {
|
||||||
header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc)
|
header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc)
|
||||||
header += " (";
|
header += " (";
|
||||||
header += parse(sender->long_name);
|
header += parse(sender->user.long_name);
|
||||||
header += ")";
|
header += ")";
|
||||||
} else
|
} else
|
||||||
header += hexifyNodeNum(latestMessage->dm.sender);
|
header += hexifyNodeNum(latestMessage->dm.sender);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ using namespace NicheGraphics;
|
|||||||
bool InkHUD::FavoritesMapApplet::shouldDrawNode(meshtastic_NodeInfoLite *node)
|
bool InkHUD::FavoritesMapApplet::shouldDrawNode(meshtastic_NodeInfoLite *node)
|
||||||
{
|
{
|
||||||
// Keep our own node available as map anchor/center; all others must be favorited.
|
// Keep our own node available as map anchor/center; all others must be favorited.
|
||||||
return node && (node->num == nodeDB->getNodeNum() || nodeInfoLiteIsFavorite(node));
|
return node && (node->num == nodeDB->getNodeNum() || node->is_favorite);
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::FavoritesMapApplet::onRender(bool full)
|
void InkHUD::FavoritesMapApplet::onRender(bool full)
|
||||||
@@ -25,7 +25,7 @@ void InkHUD::FavoritesMapApplet::onRender(bool full)
|
|||||||
|
|
||||||
// Draw our latest "node of interest" as a special marker.
|
// Draw our latest "node of interest" as a special marker.
|
||||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(lastFrom);
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(lastFrom);
|
||||||
if (node && nodeInfoLiteIsFavorite(node) && nodeDB->hasValidPosition(node) && enoughMarkers())
|
if (node && node->is_favorite && nodeDB->hasValidPosition(node) && enoughMarkers())
|
||||||
drawLabeledMarker(node);
|
drawLabeledMarker(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ ProcessMessage InkHUD::FavoritesMapApplet::handleReceived(const meshtastic_MeshP
|
|||||||
} else {
|
} else {
|
||||||
// For non-local packets, this applet only reacts to favorited nodes.
|
// For non-local packets, this applet only reacts to favorited nodes.
|
||||||
const meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(mp.from);
|
const meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(mp.from);
|
||||||
if (!nodeInfoLiteIsFavorite(sender))
|
if (!sender || !sender->is_favorite)
|
||||||
return ProcessMessage::CONTINUE;
|
return ProcessMessage::CONTINUE;
|
||||||
|
|
||||||
// Check if this position is from someone different than our previous position packet.
|
// Check if this position is from someone different than our previous position packet.
|
||||||
|
|||||||
@@ -80,8 +80,8 @@ void InkHUD::HeardApplet::populateFromNodeDB()
|
|||||||
ordered.resize(maxCards());
|
ordered.resize(maxCards());
|
||||||
|
|
||||||
// Create card info for these (stale) node observations
|
// Create card info for these (stale) node observations
|
||||||
const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||||
for (const meshtastic_NodeInfoLite *node : ordered) {
|
for (meshtastic_NodeInfoLite *node : ordered) {
|
||||||
CardInfo c;
|
CardInfo c;
|
||||||
c.nodeNum = node->num;
|
c.nodeNum = node->num;
|
||||||
|
|
||||||
@@ -89,16 +89,14 @@ void InkHUD::HeardApplet::populateFromNodeDB()
|
|||||||
c.hopsAway = node->hops_away;
|
c.hopsAway = node->hops_away;
|
||||||
|
|
||||||
if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) {
|
if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) {
|
||||||
meshtastic_PositionLite ourPos;
|
// Get lat and long as float
|
||||||
meshtastic_PositionLite theirPos;
|
// Meshtastic stores these as integers internally
|
||||||
if (nodeDB->copyNodePosition(ourNode->num, ourPos) && nodeDB->copyNodePosition(node->num, theirPos)) {
|
float ourLat = ourNode->position.latitude_i * 1e-7;
|
||||||
float ourLat = ourPos.latitude_i * 1e-7;
|
float ourLong = ourNode->position.longitude_i * 1e-7;
|
||||||
float ourLong = ourPos.longitude_i * 1e-7;
|
float theirLat = node->position.latitude_i * 1e-7;
|
||||||
float theirLat = theirPos.latitude_i * 1e-7;
|
float theirLong = node->position.longitude_i * 1e-7;
|
||||||
float theirLong = theirPos.longitude_i * 1e-7;
|
|
||||||
|
|
||||||
c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong);
|
c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert into the card collection (member of base class)
|
// Insert into the card collection (member of base class)
|
||||||
@@ -124,4 +122,4 @@ std::string InkHUD::HeardApplet::getHeaderText()
|
|||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
@@ -13,6 +13,7 @@ RotaryEncoderImpl *rotaryEncoderImpl;
|
|||||||
|
|
||||||
RotaryEncoderImpl::RotaryEncoderImpl()
|
RotaryEncoderImpl::RotaryEncoderImpl()
|
||||||
{
|
{
|
||||||
|
rotary = nullptr;
|
||||||
#ifdef ARCH_ESP32
|
#ifdef ARCH_ESP32
|
||||||
isFirstInit = true;
|
isFirstInit = true;
|
||||||
#endif
|
#endif
|
||||||
@@ -22,6 +23,11 @@ RotaryEncoderImpl::~RotaryEncoderImpl()
|
|||||||
{
|
{
|
||||||
LOG_DEBUG("RotaryEncoderImpl destructor");
|
LOG_DEBUG("RotaryEncoderImpl destructor");
|
||||||
detachRotaryEncoderInterrupts();
|
detachRotaryEncoderInterrupts();
|
||||||
|
|
||||||
|
if (rotary != nullptr) {
|
||||||
|
delete rotary;
|
||||||
|
rotary = nullptr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RotaryEncoderImpl::init()
|
bool RotaryEncoderImpl::init()
|
||||||
@@ -37,9 +43,8 @@ bool RotaryEncoderImpl::init()
|
|||||||
eventPressed = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_press);
|
eventPressed = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_press);
|
||||||
|
|
||||||
if (rotary == nullptr) {
|
if (rotary == nullptr) {
|
||||||
rotary.reset(new RotaryEncoder(moduleConfig.canned_message.inputbroker_pin_a,
|
rotary = new RotaryEncoder(moduleConfig.canned_message.inputbroker_pin_a, moduleConfig.canned_message.inputbroker_pin_b,
|
||||||
moduleConfig.canned_message.inputbroker_pin_b,
|
moduleConfig.canned_message.inputbroker_pin_press);
|
||||||
moduleConfig.canned_message.inputbroker_pin_press));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
attachRotaryEncoderInterrupts();
|
attachRotaryEncoderInterrupts();
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
#include "InputBroker.h"
|
#include "InputBroker.h"
|
||||||
#include "concurrency/OSThread.h"
|
#include "concurrency/OSThread.h"
|
||||||
#include "mesh/NodeDB.h"
|
#include "mesh/NodeDB.h"
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
class RotaryEncoder;
|
class RotaryEncoder;
|
||||||
|
|
||||||
@@ -29,7 +28,7 @@ class RotaryEncoderImpl final : public InputPollable
|
|||||||
input_broker_event eventCcw = INPUT_BROKER_NONE;
|
input_broker_event eventCcw = INPUT_BROKER_NONE;
|
||||||
input_broker_event eventPressed = INPUT_BROKER_NONE;
|
input_broker_event eventPressed = INPUT_BROKER_NONE;
|
||||||
|
|
||||||
std::unique_ptr<RotaryEncoder> rotary;
|
RotaryEncoder *rotary;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
#ifdef ARCH_ESP32
|
#ifdef ARCH_ESP32
|
||||||
|
|||||||
+1
-8
@@ -731,15 +731,8 @@ void setup()
|
|||||||
#elif defined(USE_SH1107_128_64)
|
#elif defined(USE_SH1107_128_64)
|
||||||
screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // keep dimension of 128x64
|
screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // keep dimension of 128x64
|
||||||
#else
|
#else
|
||||||
if (config.display.oled != meshtastic_Config_DisplayConfig_OledType_OLED_AUTO) {
|
if (config.display.oled != meshtastic_Config_DisplayConfig_OledType_OLED_AUTO)
|
||||||
screen_model = config.display.oled;
|
screen_model = config.display.oled;
|
||||||
|
|
||||||
// Fix: update geometry for SH1107 128x128 selected via menu
|
|
||||||
if (screen_model == meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_128) {
|
|
||||||
screen_geometry = GEOMETRY_128_128;
|
|
||||||
screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // normalize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ bool CryptoEngine::ensurePkiKeys(meshtastic_Config_SecurityConfig &security, mes
|
|||||||
* @param bytes Buffer containing plaintext input.
|
* @param bytes Buffer containing plaintext input.
|
||||||
* @param bytesOut Output buffer to be populated with encrypted ciphertext.
|
* @param bytesOut Output buffer to be populated with encrypted ciphertext.
|
||||||
*/
|
*/
|
||||||
bool CryptoEngine::encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_NodeInfoLite_public_key_t remotePublic,
|
bool CryptoEngine::encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic,
|
||||||
uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut)
|
uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut)
|
||||||
{
|
{
|
||||||
uint8_t *auth;
|
uint8_t *auth;
|
||||||
@@ -152,7 +152,7 @@ bool CryptoEngine::encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtas
|
|||||||
* @param bytes Buffer containing ciphertext input.
|
* @param bytes Buffer containing ciphertext input.
|
||||||
* @param bytesOut Output buffer to be populated with decrypted plaintext.
|
* @param bytesOut Output buffer to be populated with decrypted plaintext.
|
||||||
*/
|
*/
|
||||||
bool CryptoEngine::decryptCurve25519(uint32_t fromNode, meshtastic_NodeInfoLite_public_key_t remotePublic, uint64_t packetNum,
|
bool CryptoEngine::decryptCurve25519(uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, uint64_t packetNum,
|
||||||
size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut)
|
size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut)
|
||||||
{
|
{
|
||||||
const uint8_t *auth = bytes + numBytes - 12; // set to last 8 bytes of text?
|
const uint8_t *auth = bytes + numBytes - 12; // set to last 8 bytes of text?
|
||||||
|
|||||||
@@ -40,12 +40,9 @@ class CryptoEngine
|
|||||||
|
|
||||||
#endif
|
#endif
|
||||||
void setDHPrivateKey(uint8_t *_private_key);
|
void setDHPrivateKey(uint8_t *_private_key);
|
||||||
// The remotePublic key parameter takes the public_key bytes container from
|
virtual bool encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic,
|
||||||
// a stored node header. NodeInfoLite is the on-device storage type since
|
|
||||||
// the slim refactor flattened UserLite into it.
|
|
||||||
virtual bool encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_NodeInfoLite_public_key_t remotePublic,
|
|
||||||
uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut);
|
uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut);
|
||||||
virtual bool decryptCurve25519(uint32_t fromNode, meshtastic_NodeInfoLite_public_key_t remotePublic, uint64_t packetNum,
|
virtual bool decryptCurve25519(uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, uint64_t packetNum,
|
||||||
size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut);
|
size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut);
|
||||||
virtual bool setDHPublicKey(uint8_t *publicKey);
|
virtual bool setDHPublicKey(uint8_t *publicKey);
|
||||||
virtual void hash(uint8_t *bytes, size_t numBytes);
|
virtual void hash(uint8_t *bytes, size_t numBytes);
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
#include "configuration.h"
|
|
||||||
|
|
||||||
#include "LR11x0Interface.cpp"
|
#include "LR11x0Interface.cpp"
|
||||||
#include "LR11x0Interface.h"
|
#include "LR11x0Interface.h"
|
||||||
#include "LR20x0Interface.cpp"
|
|
||||||
#include "LR20x0Interface.h"
|
|
||||||
#include "SX126xInterface.cpp"
|
#include "SX126xInterface.cpp"
|
||||||
#include "SX126xInterface.h"
|
#include "SX126xInterface.h"
|
||||||
#include "SX128xInterface.cpp"
|
#include "SX128xInterface.cpp"
|
||||||
@@ -25,9 +21,6 @@ template class LR11x0Interface<LR1110>;
|
|||||||
template class LR11x0Interface<LR1120>;
|
template class LR11x0Interface<LR1120>;
|
||||||
template class LR11x0Interface<LR1121>;
|
template class LR11x0Interface<LR1121>;
|
||||||
#endif
|
#endif
|
||||||
#if defined(USE_LR2021) && RADIOLIB_EXCLUDE_LR2021 != 1
|
|
||||||
template class LR20x0Interface<LR2021>;
|
|
||||||
#endif
|
|
||||||
#ifdef ARCH_STM32WL
|
#ifdef ARCH_STM32WL
|
||||||
template class SX126xInterface<STM32WLx>;
|
template class SX126xInterface<STM32WLx>;
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -57,19 +57,16 @@ template <typename T> bool LR11x0Interface<T>::init()
|
|||||||
#if ARCH_PORTDUINO
|
#if ARCH_PORTDUINO
|
||||||
float tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000;
|
float tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000;
|
||||||
// FIXME: correct logic to default to not using TCXO if no voltage is specified for LR11x0_DIO3_TCXO_VOLTAGE
|
// FIXME: correct logic to default to not using TCXO if no voltage is specified for LR11x0_DIO3_TCXO_VOLTAGE
|
||||||
#elif defined(LR11X0_DIO3_TCXO_VOLTAGE)
|
#elif !defined(LR11X0_DIO3_TCXO_VOLTAGE)
|
||||||
float tcxoVoltage = LR11X0_DIO3_TCXO_VOLTAGE;
|
|
||||||
LOG_DEBUG("LR11X0_DIO3_TCXO_VOLTAGE defined, using DIO3 as TCXO reference voltage at %f V", LR11X0_DIO3_TCXO_VOLTAGE);
|
|
||||||
// (DIO3 is not free to be used as an IRQ)
|
|
||||||
#elif defined(TCXO_OPTIONAL)
|
|
||||||
float tcxoVoltage = 1.6f; // TCXO_OPTIONAL: try default 1.6 V first, fall back to XTAL on failure
|
|
||||||
LOG_DEBUG("TCXO_OPTIONAL: no LR11X0_DIO3_TCXO_VOLTAGE defined, trying default TCXO Vref 1.6 V first");
|
|
||||||
#else
|
|
||||||
float tcxoVoltage =
|
float tcxoVoltage =
|
||||||
0; // "TCXO reference voltage to be set on DIO3. Defaults to 1.6 V, set to 0 to skip." per
|
0; // "TCXO reference voltage to be set on DIO3. Defaults to 1.6 V, set to 0 to skip." per
|
||||||
// https://github.com/jgromes/RadioLib/blob/690a050ebb46e6097c5d00c371e961c1caa3b52e/src/modules/LR11x0/LR11x0.h#L471C26-L471C104
|
// https://github.com/jgromes/RadioLib/blob/690a050ebb46e6097c5d00c371e961c1caa3b52e/src/modules/LR11x0/LR11x0.h#L471C26-L471C104
|
||||||
// (DIO3 is free to be used as an IRQ)
|
// (DIO3 is free to be used as an IRQ)
|
||||||
LOG_DEBUG("LR11X0_DIO3_TCXO_VOLTAGE not defined, not using DIO3 as TCXO reference voltage");
|
LOG_DEBUG("LR11X0_DIO3_TCXO_VOLTAGE not defined, not using DIO3 as TCXO reference voltage");
|
||||||
|
#else
|
||||||
|
float tcxoVoltage = LR11X0_DIO3_TCXO_VOLTAGE;
|
||||||
|
LOG_DEBUG("LR11X0_DIO3_TCXO_VOLTAGE defined, using DIO3 as TCXO reference voltage at %f V", LR11X0_DIO3_TCXO_VOLTAGE);
|
||||||
|
// (DIO3 is not free to be used as an IRQ)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
RadioLibInterface::init();
|
RadioLibInterface::init();
|
||||||
@@ -104,17 +101,6 @@ template <typename T> bool LR11x0Interface<T>::init()
|
|||||||
res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
|
res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
|
||||||
}
|
}
|
||||||
|
|
||||||
#if defined(TCXO_OPTIONAL)
|
|
||||||
// If init failed for any reason other than chip not found, retry without TCXO (XTAL mode)
|
|
||||||
if (res != RADIOLIB_ERR_NONE && res != RADIOLIB_ERR_CHIP_NOT_FOUND && tcxoVoltage > 0) {
|
|
||||||
LOG_WARN("LR11x0 init failed with TCXO Vref %f V (err %d), retrying without TCXO", tcxoVoltage, res);
|
|
||||||
tcxoVoltage = 0;
|
|
||||||
res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
|
|
||||||
if (res == RADIOLIB_ERR_NONE)
|
|
||||||
LOG_INFO("LR11x0 init success without TCXO (XTAL mode)");
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// \todo Display actual typename of the adapter, not just `LR11x0`
|
// \todo Display actual typename of the adapter, not just `LR11x0`
|
||||||
LOG_INFO("LR11x0 init result %d", res);
|
LOG_INFO("LR11x0 init result %d", res);
|
||||||
if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED)
|
if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED)
|
||||||
@@ -212,7 +198,7 @@ template <typename T> bool LR11x0Interface<T>::reconfigure()
|
|||||||
|
|
||||||
startReceive(); // restart receiving
|
startReceive(); // restart receiving
|
||||||
|
|
||||||
return true;
|
return RADIOLIB_ERR_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename T> void LR11x0Interface<T>::disableInterrupt()
|
template <typename T> void LR11x0Interface<T>::disableInterrupt()
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
#include "configuration.h"
|
|
||||||
|
|
||||||
#if defined(USE_LR2021) && RADIOLIB_EXCLUDE_LR2021 != 1
|
|
||||||
|
|
||||||
#include "LR2021Interface.h"
|
|
||||||
#include "error.h"
|
|
||||||
|
|
||||||
LR2021Interface::LR2021Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst,
|
|
||||||
RADIOLIB_PIN_TYPE busy)
|
|
||||||
: LR20x0Interface(hal, cs, irq, rst, busy)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
bool LR2021Interface::wideLora()
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#if RADIOLIB_EXCLUDE_LR2021 != 1
|
|
||||||
#include "LR20x0Interface.h"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Our adapter for LR2021 radios
|
|
||||||
*/
|
|
||||||
class LR2021Interface : public LR20x0Interface<LR2021>
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
LR2021Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst,
|
|
||||||
RADIOLIB_PIN_TYPE busy);
|
|
||||||
bool wideLora() override;
|
|
||||||
};
|
|
||||||
#endif
|
|
||||||
@@ -1,399 +0,0 @@
|
|||||||
#include "configuration.h"
|
|
||||||
|
|
||||||
#if defined(USE_LR2021) && RADIOLIB_EXCLUDE_LR2021 != 1
|
|
||||||
#include "LR20x0Interface.h"
|
|
||||||
#include "error.h"
|
|
||||||
#include "mesh/NodeDB.h"
|
|
||||||
|
|
||||||
// Keep LR20x0 naming while RadioLib exposes LR2021 symbols.
|
|
||||||
#ifndef LR20x0
|
|
||||||
#define LR20x0 LR2021
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef LR2021_DIO_AS_RF_SWITCH
|
|
||||||
#include "rfswitch.h"
|
|
||||||
#elif ARCH_PORTDUINO
|
|
||||||
#include "PortduinoGlue.h"
|
|
||||||
#define lr20x0_rfswitch_dio_pins portduino_config.rfswitch_dio_pins
|
|
||||||
#define lr20x0_rfswitch_table portduino_config.rfswitch_table
|
|
||||||
#else
|
|
||||||
static const uint32_t lr20x0_rfswitch_dio_pins[] = {RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC};
|
|
||||||
static const Module::RfSwitchMode_t lr20x0_rfswitch_table[] = {
|
|
||||||
{LR20x0::MODE_STBY, {}}, {LR20x0::MODE_RX, {}}, {LR20x0::MODE_TX, {}},
|
|
||||||
{LR20x0::MODE_RX_HF, {}}, {LR20x0::MODE_TX_HF, {}}, END_OF_MODE_TABLE,
|
|
||||||
};
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Particular boards might define a different max power based on what their hardware can do, default to max power output if not
|
|
||||||
// specified (may be dangerous if using external PA and LR20x0 power config forgotten)
|
|
||||||
#if ARCH_PORTDUINO
|
|
||||||
#define LR2021_MAX_POWER portduino_config.lr2021_max_power
|
|
||||||
#endif
|
|
||||||
#ifndef LR2021_MAX_POWER
|
|
||||||
#define LR2021_MAX_POWER 22
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// the 2.4G part maxes at 12dBm
|
|
||||||
|
|
||||||
#if ARCH_PORTDUINO
|
|
||||||
#define LR2021_MAX_POWER_HF portduino_config.lr2021_max_power_hf
|
|
||||||
#endif
|
|
||||||
#ifndef LR2021_MAX_POWER_HF
|
|
||||||
#define LR2021_MAX_POWER_HF 12
|
|
||||||
#endif
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
LR20x0Interface<T>::LR20x0Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst,
|
|
||||||
RADIOLIB_PIN_TYPE busy)
|
|
||||||
: RadioLibInterface(hal, cs, irq, rst, busy, &lora), lora(&module)
|
|
||||||
{
|
|
||||||
LOG_WARN("LR20x0Interface(cs=%d, irq=%d, rst=%d, busy=%d)", cs, irq, rst, busy);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initialise the Driver transport hardware and software.
|
|
||||||
/// Make sure the Driver is properly configured before calling init().
|
|
||||||
/// \return true if initialisation succeeded.
|
|
||||||
template <typename T> bool LR20x0Interface<T>::init()
|
|
||||||
{
|
|
||||||
#ifdef LR2021_POWER_EN
|
|
||||||
pinMode(LR2021_POWER_EN, OUTPUT);
|
|
||||||
digitalWrite(LR2021_POWER_EN, HIGH);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if ARCH_PORTDUINO
|
|
||||||
float tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000;
|
|
||||||
// FIXME: correct logic to default to not using TCXO if no voltage is specified for LR20x0_DIO3_TCXO_VOLTAGE
|
|
||||||
#elif defined(LR2021_DIO3_TCXO_VOLTAGE)
|
|
||||||
float tcxoVoltage = LR2021_DIO3_TCXO_VOLTAGE;
|
|
||||||
LOG_DEBUG("LR2021_DIO3_TCXO_VOLTAGE defined, using DIO3 as TCXO reference voltage at %f V", LR2021_DIO3_TCXO_VOLTAGE);
|
|
||||||
// (DIO3 is not free to be used as an IRQ)
|
|
||||||
#elif defined(TCXO_OPTIONAL)
|
|
||||||
float tcxoVoltage = 1.6f; // TCXO_OPTIONAL: try default 1.6 V first, fall back to XTAL on failure
|
|
||||||
LOG_DEBUG("TCXO_OPTIONAL: no LR2021_DIO3_TCXO_VOLTAGE defined, trying default TCXO Vref 1.6 V first");
|
|
||||||
#else
|
|
||||||
float tcxoVoltage =
|
|
||||||
0; // "TCXO reference voltage to be set on DIO3. Defaults to 1.6 V, set to 0 to skip." per
|
|
||||||
// https://github.com/jgromes/RadioLib/blob/690a050ebb46e6097c5d00c371e961c1caa3b52e/src/modules/LR11x0/LR11x0.h#L471C26-L471C104
|
|
||||||
// (DIO3 is free to be used as an IRQ)
|
|
||||||
LOG_DEBUG("LR2021_DIO3_TCXO_VOLTAGE not defined, not using DIO3 as TCXO reference voltage");
|
|
||||||
#endif
|
|
||||||
|
|
||||||
RadioLibInterface::init();
|
|
||||||
|
|
||||||
#ifdef LR2021_IRQ_DIO_NUM
|
|
||||||
lora.irqDioNum = LR2021_IRQ_DIO_NUM;
|
|
||||||
LOG_DEBUG("Set irqDioNum %d", lora.irqDioNum);
|
|
||||||
#elif defined(IRQ_DIO_NUM)
|
|
||||||
lora.irqDioNum = IRQ_DIO_NUM;
|
|
||||||
LOG_DEBUG("Set irqDioNum %d", lora.irqDioNum);
|
|
||||||
#else
|
|
||||||
LOG_DEBUG("Use default irqDioNum %d", lora.irqDioNum);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) { // clamp if wide freq range
|
|
||||||
limitPower(LR2021_MAX_POWER_HF);
|
|
||||||
} else {
|
|
||||||
limitPower(LR2021_MAX_POWER); // default clamp for non-wide freq range
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef LR2021_RF_SWITCH_SUBGHZ
|
|
||||||
pinMode(LR2021_RF_SWITCH_SUBGHZ, OUTPUT);
|
|
||||||
digitalWrite(LR2021_RF_SWITCH_SUBGHZ, getFreq() < 1e9 ? HIGH : LOW);
|
|
||||||
LOG_DEBUG("Set RF0 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz");
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef LR2021_RF_SWITCH_2_4GHZ
|
|
||||||
pinMode(LR2021_RF_SWITCH_2_4GHZ, OUTPUT);
|
|
||||||
digitalWrite(LR2021_RF_SWITCH_2_4GHZ, getFreq() < 1e9 ? LOW : HIGH);
|
|
||||||
LOG_DEBUG("Set RF1 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz");
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Allow extra time for TCXO to stabilize after power-on
|
|
||||||
delay(10);
|
|
||||||
|
|
||||||
int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
|
|
||||||
|
|
||||||
// Retry if we get SPI command failed - some units need extra TCXO stabilization time
|
|
||||||
if (res == RADIOLIB_ERR_SPI_CMD_FAILED) {
|
|
||||||
LOG_WARN("LR20x0 init failed with %d (SPI_CMD_FAILED), retrying after delay...", res);
|
|
||||||
delay(100);
|
|
||||||
res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
|
|
||||||
}
|
|
||||||
|
|
||||||
#if defined(TCXO_OPTIONAL)
|
|
||||||
// If init failed for any reason other than chip not found, retry without TCXO (XTAL mode)
|
|
||||||
if (res != RADIOLIB_ERR_NONE && res != RADIOLIB_ERR_CHIP_NOT_FOUND && tcxoVoltage > 0) {
|
|
||||||
LOG_WARN("LR20x0 init failed with TCXO Vref %f V (err %d), retrying without TCXO", tcxoVoltage, res);
|
|
||||||
tcxoVoltage = 0;
|
|
||||||
res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
|
|
||||||
if (res == RADIOLIB_ERR_NONE)
|
|
||||||
LOG_INFO("LR20x0 init success without TCXO (XTAL mode)");
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// \todo Display actual typename of the adapter, not just `LR20x0`
|
|
||||||
LOG_INFO("LR20x0 init result %d", res);
|
|
||||||
if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Some basic info about the module's explicit firmware version - no other info available
|
|
||||||
// Currently requires radiolib godmode
|
|
||||||
|
|
||||||
#if RADIOLIB_GODMODE
|
|
||||||
uint8_t fwMajor = 0;
|
|
||||||
uint8_t fwMinor = 0;
|
|
||||||
int versionRes = lora.getVersion(&fwMajor, &fwMinor);
|
|
||||||
if (versionRes == RADIOLIB_ERR_NONE)
|
|
||||||
LOG_DEBUG("LR20x0 FW %d.%d", fwMajor, fwMinor);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
LOG_INFO("Frequency set to %f", getFreq());
|
|
||||||
LOG_INFO("Bandwidth set to %f", bw);
|
|
||||||
LOG_INFO("Power output set to %d", power);
|
|
||||||
|
|
||||||
if (res == RADIOLIB_ERR_NONE)
|
|
||||||
res = lora.setCRC(2);
|
|
||||||
|
|
||||||
// Standard DCDC ramp timing from RadioLib workarounds (register 0x00F20024)
|
|
||||||
// Currently requires radiolib godmode
|
|
||||||
#if RADIOLIB_GODMODE
|
|
||||||
if (res == RADIOLIB_ERR_NONE) {
|
|
||||||
uint8_t rampTimes[4] = {15, 15, 15, 15}; // Standard case for all conditions
|
|
||||||
res = lora.setRegMode(RADIOLIB_LR2021_REG_MODE_SIMO_NORMAL, rampTimes);
|
|
||||||
if (res != RADIOLIB_ERR_NONE)
|
|
||||||
LOG_WARN("LR2021 setRegMode failed: %d", res);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef LR2021_DIO_AS_RF_SWITCH
|
|
||||||
bool dioAsRfSwitch = true;
|
|
||||||
#elif defined(ARCH_PORTDUINO)
|
|
||||||
bool dioAsRfSwitch = portduino_config.has_rfswitch_table;
|
|
||||||
#else
|
|
||||||
bool dioAsRfSwitch = false;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (dioAsRfSwitch) {
|
|
||||||
lora.setRfSwitchTable(lr20x0_rfswitch_dio_pins, lr20x0_rfswitch_table);
|
|
||||||
LOG_DEBUG("Set DIO RF switch");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res == RADIOLIB_ERR_NONE) {
|
|
||||||
if (config.lora.sx126x_rx_boosted_gain) { // the name is unfortunate but historically accurate
|
|
||||||
res = lora.setRxBoostedGainMode(true);
|
|
||||||
LOG_INFO("Set RX gain to boosted mode; result: %d", res);
|
|
||||||
} else {
|
|
||||||
res = lora.setRxBoostedGainMode(false);
|
|
||||||
LOG_INFO("Set RX gain to power saving mode (boosted mode off); result: %d", res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res == RADIOLIB_ERR_NONE)
|
|
||||||
startReceive(); // start receiving
|
|
||||||
|
|
||||||
return res == RADIOLIB_ERR_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T> bool LR20x0Interface<T>::reconfigure()
|
|
||||||
{
|
|
||||||
RadioLibInterface::reconfigure();
|
|
||||||
|
|
||||||
// set mode to standby
|
|
||||||
setStandby();
|
|
||||||
|
|
||||||
// configure publicly accessible settings
|
|
||||||
int err = lora.setSpreadingFactor(sf);
|
|
||||||
if (err != RADIOLIB_ERR_NONE)
|
|
||||||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
|
||||||
|
|
||||||
err = lora.setBandwidth(bw); // different form than LR11xx
|
|
||||||
if (err != RADIOLIB_ERR_NONE)
|
|
||||||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
|
||||||
|
|
||||||
err = lora.setCodingRate(cr, cr != 7); // use long interleaving except if CR is 4/7 which doesn't support it
|
|
||||||
if (err != RADIOLIB_ERR_NONE)
|
|
||||||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
|
||||||
|
|
||||||
err = lora.setSyncWord(syncWord);
|
|
||||||
assert(err == RADIOLIB_ERR_NONE);
|
|
||||||
|
|
||||||
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) { // clamp if wide freq range
|
|
||||||
limitPower(LR2021_MAX_POWER_HF);
|
|
||||||
} else {
|
|
||||||
limitPower(LR2021_MAX_POWER); // default clamp for non-wide freq range
|
|
||||||
}
|
|
||||||
|
|
||||||
err = lora.setPreambleLength(preambleLength);
|
|
||||||
assert(err == RADIOLIB_ERR_NONE);
|
|
||||||
|
|
||||||
err = lora.setFrequency(getFreq());
|
|
||||||
if (err != RADIOLIB_ERR_NONE)
|
|
||||||
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
|
||||||
|
|
||||||
err = lora.setOutputPower(power);
|
|
||||||
assert(err == RADIOLIB_ERR_NONE);
|
|
||||||
|
|
||||||
// Apply RX gain mode — valid in STDBY, matches resetAGC() pattern
|
|
||||||
err = lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain);
|
|
||||||
if (err != RADIOLIB_ERR_NONE)
|
|
||||||
LOG_WARN("LR20x0 setRxBoostedGainMode %s%d", radioLibErr, err);
|
|
||||||
|
|
||||||
startReceive(); // restart receiving
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T> void LR20x0Interface<T>::disableInterrupt()
|
|
||||||
{
|
|
||||||
lora.clearIrqAction();
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T> void LR20x0Interface<T>::setStandby()
|
|
||||||
{
|
|
||||||
checkNotification(); // handle any pending interrupts before we force standby
|
|
||||||
|
|
||||||
int err = lora.standby();
|
|
||||||
|
|
||||||
if (err != RADIOLIB_ERR_NONE) {
|
|
||||||
LOG_DEBUG("LR20x0 standby failed with error %d", err);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(err == RADIOLIB_ERR_NONE);
|
|
||||||
|
|
||||||
isReceiving = false; // If we were receiving, not any more
|
|
||||||
activeReceiveStart = 0;
|
|
||||||
disableInterrupt();
|
|
||||||
completeSending(); // If we were sending, not anymore
|
|
||||||
RadioLibInterface::setStandby();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add SNR data to received messages
|
|
||||||
*/
|
|
||||||
template <typename T> void LR20x0Interface<T>::addReceiveMetadata(meshtastic_MeshPacket *mp)
|
|
||||||
{
|
|
||||||
// LOG_DEBUG("PacketStatus %x", lora.getPacketStatus());
|
|
||||||
mp->rx_snr = lora.getSNR();
|
|
||||||
mp->rx_rssi = lround(lora.getRSSI());
|
|
||||||
// LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError()); // not implemented for LR20x0, but noop for LR11x0
|
|
||||||
// too(!)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** We override to turn on transmitter power as needed.
|
|
||||||
*/
|
|
||||||
template <typename T> void LR20x0Interface<T>::configHardwareForSend()
|
|
||||||
{
|
|
||||||
RadioLibInterface::configHardwareForSend();
|
|
||||||
}
|
|
||||||
|
|
||||||
// For power draw measurements, helpful to force radio to stay sleeping
|
|
||||||
// #define SLEEP_ONLY
|
|
||||||
|
|
||||||
template <typename T> void LR20x0Interface<T>::startReceive()
|
|
||||||
{
|
|
||||||
#ifdef SLEEP_ONLY
|
|
||||||
sleep();
|
|
||||||
#else
|
|
||||||
|
|
||||||
setStandby();
|
|
||||||
|
|
||||||
lora.setPreambleLength(preambleLength); // Solve RX ack fail after direct message sent. Not sure why this is needed.
|
|
||||||
|
|
||||||
// We use a 16 bit preamble so this should save some power by letting radio sit in standby mostly.
|
|
||||||
int err =
|
|
||||||
lora.startReceive(RADIOLIB_LR2021_RX_TIMEOUT_INF, MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS, RADIOLIB_IRQ_RX_DEFAULT_MASK, 0);
|
|
||||||
if (err)
|
|
||||||
LOG_ERROR("StartReceive error: %d", err);
|
|
||||||
assert(err == RADIOLIB_ERR_NONE);
|
|
||||||
|
|
||||||
RadioLibInterface::startReceive();
|
|
||||||
|
|
||||||
// Must be done AFTER starting receive, because startReceive clears (possibly stale) interrupt pending register bits
|
|
||||||
enableInterrupt(isrRxLevel0);
|
|
||||||
checkRxDoneIrqFlag();
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Is the channel currently active? */
|
|
||||||
template <typename T> bool LR20x0Interface<T>::isChannelActive()
|
|
||||||
{
|
|
||||||
// check if we can detect a LoRa preamble on the current channel
|
|
||||||
ChannelScanConfig_t cfg = {.cad = {.symNum = NUM_SYM_CAD,
|
|
||||||
.detPeak = RADIOLIB_LR2021_CAD_PARAM_DEFAULT,
|
|
||||||
.detMin = RADIOLIB_LR2021_CAD_PARAM_DEFAULT,
|
|
||||||
.exitMode = RADIOLIB_LR2021_CAD_PARAM_DEFAULT,
|
|
||||||
.timeout = 0,
|
|
||||||
.irqFlags = RADIOLIB_IRQ_CAD_DEFAULT_FLAGS,
|
|
||||||
.irqMask = RADIOLIB_IRQ_CAD_DEFAULT_MASK}};
|
|
||||||
int16_t result;
|
|
||||||
|
|
||||||
setStandby();
|
|
||||||
result = lora.scanChannel(cfg);
|
|
||||||
if (result == RADIOLIB_LORA_DETECTED)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
assert(result != RADIOLIB_ERR_WRONG_MODEM);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Could we send right now (i.e. either not actively receiving or transmitting)? */
|
|
||||||
template <typename T> bool LR20x0Interface<T>::isActivelyReceiving()
|
|
||||||
{
|
|
||||||
// The IRQ status will be cleared when we start our read operation. Check if we've started a header, but haven't yet
|
|
||||||
// received and handled the interrupt for reading the packet/handling errors.
|
|
||||||
return receiveDetected(lora.getIrqStatus(), RADIOLIB_LR2021_IRQ_LORA_HEADER_VALID, RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED);
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef LR20X0_AGC_RESET
|
|
||||||
template <typename T> void LR20x0Interface<T>::resetAGC()
|
|
||||||
{
|
|
||||||
// Safety: don't reset mid-packet
|
|
||||||
if (sendingPacket != NULL || (isReceiving && isActivelyReceiving()))
|
|
||||||
return;
|
|
||||||
|
|
||||||
LOG_DEBUG("LR20x0 AGC reset: warm sleep + Calibrate(0x3F)");
|
|
||||||
|
|
||||||
// 1. Warm sleep — powers down the analog frontend, resetting AGC state
|
|
||||||
lora.sleep(true, 0);
|
|
||||||
|
|
||||||
// 2. Wake to RC standby for stable calibration
|
|
||||||
lora.standby(RADIOLIB_LR20X0_STANDBY_RC, true);
|
|
||||||
|
|
||||||
// 3. Calibrate all blocks (PLL, ADC, image, RC oscillators)
|
|
||||||
// calibrate() is protected on LR20x0, so use raw SPI (same as internal implementation)
|
|
||||||
uint8_t calData = RADIOLIB_LR20X0_CALIBRATE_ALL;
|
|
||||||
module.SPIwriteStream(RADIOLIB_LR20X0_CMD_CALIBRATE, &calData, 1, true, true);
|
|
||||||
|
|
||||||
// 4. Re-calibrate image rejection for actual operating frequency
|
|
||||||
// Calibrate(0x3F) defaults to 902-928 MHz which is wrong for other regions.
|
|
||||||
lora.calibrateImageRejection(getFreq() - 4.0f, getFreq() + 4.0f);
|
|
||||||
|
|
||||||
// 5. Re-apply RX boosted gain mode
|
|
||||||
lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain);
|
|
||||||
|
|
||||||
// 6. Resume receiving
|
|
||||||
startReceive();
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
template <typename T> bool LR20x0Interface<T>::sleep()
|
|
||||||
{
|
|
||||||
// \todo Display actual typename of the adapter, not just `LR20x0`
|
|
||||||
LOG_DEBUG("LR20x0 entering sleep mode");
|
|
||||||
setStandby(); // Stop any pending operations
|
|
||||||
|
|
||||||
// turn off TCXO if it was powered
|
|
||||||
lora.setTCXO(0);
|
|
||||||
|
|
||||||
// put chipset into sleep mode (we've already disabled interrupts by now)
|
|
||||||
bool keepConfig = false;
|
|
||||||
lora.sleep(keepConfig, 0); // Note: we do not keep the config, full reinit will be needed
|
|
||||||
|
|
||||||
#ifdef LR2021_POWER_EN
|
|
||||||
digitalWrite(LR2021_POWER_EN, LOW);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#if RADIOLIB_EXCLUDE_LR2021 != 1
|
|
||||||
#include "RadioLibInterface.h"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* \brief Adapter for LR20x0 radio family. Implements common logic for child classes.
|
|
||||||
* \tparam T RadioLib module type for LR20x0, e.g. LR2021.
|
|
||||||
*/
|
|
||||||
template <class T> class LR20x0Interface : public RadioLibInterface
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
LR20x0Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst,
|
|
||||||
RADIOLIB_PIN_TYPE busy);
|
|
||||||
|
|
||||||
/// Initialise the Driver transport hardware and software.
|
|
||||||
/// Make sure the Driver is properly configured before calling init().
|
|
||||||
/// \return true if initialisation succeeded.
|
|
||||||
virtual bool init() override;
|
|
||||||
|
|
||||||
/// Apply any radio provisioning changes
|
|
||||||
/// Make sure the Driver is properly configured before calling init().
|
|
||||||
/// \return true if initialisation succeeded.
|
|
||||||
virtual bool reconfigure() override;
|
|
||||||
|
|
||||||
/// Prepare hardware for sleep. Call this _only_ for deep sleep, not needed for light sleep.
|
|
||||||
virtual bool sleep() override;
|
|
||||||
|
|
||||||
bool isIRQPending() override { return lora.getIrqFlags() != 0; }
|
|
||||||
|
|
||||||
#ifdef LR20X0_AGC_RESET
|
|
||||||
void resetAGC() override;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
protected:
|
|
||||||
/**
|
|
||||||
* Specific module instance
|
|
||||||
*/
|
|
||||||
T lora;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Glue functions called from ISR land
|
|
||||||
*/
|
|
||||||
virtual void disableInterrupt() override;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable a particular ISR callback glue function
|
|
||||||
*/
|
|
||||||
virtual void enableInterrupt(void (*callback)()) { lora.setIrqAction(callback); }
|
|
||||||
|
|
||||||
/** can we detect a LoRa preamble on the current channel? */
|
|
||||||
virtual bool isChannelActive() override;
|
|
||||||
|
|
||||||
/** are we actively receiving a packet (only called during receiving state) */
|
|
||||||
virtual bool isActivelyReceiving() override;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start waiting to receive a message
|
|
||||||
*/
|
|
||||||
virtual void startReceive() override;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We override to turn on transmitter power as needed.
|
|
||||||
*/
|
|
||||||
virtual void configHardwareForSend() override;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add SNR data to received messages
|
|
||||||
*/
|
|
||||||
virtual void addReceiveMetadata(meshtastic_MeshPacket *mp) override;
|
|
||||||
|
|
||||||
virtual void setStandby() override;
|
|
||||||
|
|
||||||
uint32_t getPacketTime(uint32_t pl, bool received) override { return computePacketTime(lora, pl, received); }
|
|
||||||
};
|
|
||||||
#endif
|
|
||||||
+12
-12
@@ -94,9 +94,8 @@ int MeshService::handleFromRadio(const meshtastic_MeshPacket *mp)
|
|||||||
mp->decoded.portnum == meshtastic_PortNum_TELEMETRY_APP && mp->decoded.request_id > 0) {
|
mp->decoded.portnum == meshtastic_PortNum_TELEMETRY_APP && mp->decoded.request_id > 0) {
|
||||||
LOG_DEBUG("Received telemetry response. Skip sending our NodeInfo");
|
LOG_DEBUG("Received telemetry response. Skip sending our NodeInfo");
|
||||||
// ignore our request for its NodeInfo
|
// ignore our request for its NodeInfo
|
||||||
} else if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
|
} else if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag && !nodeDB->getMeshNode(mp->from)->has_user &&
|
||||||
!nodeInfoLiteHasUser(nodeDB->getMeshNode(mp->from)) && nodeInfoModule && !isPreferredRebroadcaster &&
|
nodeInfoModule && !isPreferredRebroadcaster && !nodeDB->isFull()) {
|
||||||
!nodeDB->isFull()) {
|
|
||||||
if (airTime->isTxAllowedChannelUtil(true)) {
|
if (airTime->isTxAllowedChannelUtil(true)) {
|
||||||
const int8_t hopsUsed = getHopsAway(*mp, config.lora.hop_limit);
|
const int8_t hopsUsed = getHopsAway(*mp, config.lora.hop_limit);
|
||||||
if (hopsUsed > (int32_t)(config.lora.hop_limit + 2)) {
|
if (hopsUsed > (int32_t)(config.lora.hop_limit + 2)) {
|
||||||
@@ -393,16 +392,19 @@ meshtastic_NodeInfoLite *MeshService::refreshLocalMeshNode()
|
|||||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||||
assert(node);
|
assert(node);
|
||||||
|
|
||||||
|
// We might not have a position yet for our local node, in that case, at least try to send the time
|
||||||
|
if (!node->has_position) {
|
||||||
|
memset(&node->position, 0, sizeof(node->position));
|
||||||
|
node->has_position = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
meshtastic_PositionLite &position = node->position;
|
||||||
|
|
||||||
// Update our local node info with our time (even if we don't decide to update anyone else)
|
// Update our local node info with our time (even if we don't decide to update anyone else)
|
||||||
node->last_heard =
|
node->last_heard =
|
||||||
getValidTime(RTCQualityFromNet); // This nodedb timestamp might be stale, so update it if our clock is kinda valid
|
getValidTime(RTCQualityFromNet); // This nodedb timestamp might be stale, so update it if our clock is kinda valid
|
||||||
|
|
||||||
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
position.time = getValidTime(RTCQualityFromNet);
|
||||||
// Make sure our own NodeNum has a slot in the position map so subsequent
|
|
||||||
// updates (and the bundled NodeInfo emission to the phone) have somewhere
|
|
||||||
// to read from. Insert a default-zero entry on first call.
|
|
||||||
nodeDB->touchNodePositionTime(node->num, getValidTime(RTCQualityFromNet));
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (powerStatus->getHasBattery() == 1) {
|
if (powerStatus->getHasBattery() == 1) {
|
||||||
updateBatteryLevel(powerStatus->getBatteryChargePercent());
|
updateBatteryLevel(powerStatus->getBatteryChargePercent());
|
||||||
@@ -430,9 +432,7 @@ int MeshService::onGPSChanged(const meshtastic::GPSStatus *newStatus)
|
|||||||
// Used fixed position if configured regardless of GPS lock
|
// Used fixed position if configured regardless of GPS lock
|
||||||
if (config.position.fixed_position) {
|
if (config.position.fixed_position) {
|
||||||
LOG_WARN("Use fixed position");
|
LOG_WARN("Use fixed position");
|
||||||
meshtastic_PositionLite fixedSlot;
|
pos = TypeConversions::ConvertToPosition(node->position);
|
||||||
if (nodeDB->copyNodePosition(node->num, fixedSlot))
|
|
||||||
pos = TypeConversions::ConvertToPosition(fixedSlot);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a fresh timestamp
|
// Add a fresh timestamp
|
||||||
|
|||||||
+131
-557
@@ -22,7 +22,6 @@
|
|||||||
#include "error.h"
|
#include "error.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "mesh-pb-constants.h"
|
#include "mesh-pb-constants.h"
|
||||||
#include "mesh/generated/meshtastic/deviceonly_legacy.pb.h"
|
|
||||||
#include "meshUtils.h"
|
#include "meshUtils.h"
|
||||||
#include "modules/NeighborInfoModule.h"
|
#include "modules/NeighborInfoModule.h"
|
||||||
#include <ErriezCRC32.h>
|
#include <ErriezCRC32.h>
|
||||||
@@ -152,103 +151,24 @@ uint32_t get_st7789_id(uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_
|
|||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_t *field)
|
bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_iter_t *field)
|
||||||
{
|
{
|
||||||
const auto *iter = reinterpret_cast<const pb_field_iter_t *>(field);
|
if (ostream) {
|
||||||
switch (iter->tag) {
|
std::vector<meshtastic_NodeInfoLite> const *vec = (std::vector<meshtastic_NodeInfoLite> *)field->pData;
|
||||||
case meshtastic_NodeDatabase_nodes_tag: {
|
for (auto item : *vec) {
|
||||||
if (ostream) {
|
if (!pb_encode_tag_for_field(ostream, field))
|
||||||
const auto *vec = static_cast<const std::vector<meshtastic_NodeInfoLite> *>(iter->pData);
|
return false;
|
||||||
for (auto item : *vec) {
|
pb_encode_submessage(ostream, meshtastic_NodeInfoLite_fields, &item);
|
||||||
if (!pb_encode_tag_for_field(ostream, iter))
|
|
||||||
return false;
|
|
||||||
if (!pb_encode_submessage(ostream, meshtastic_NodeInfoLite_fields, &item))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (istream) {
|
|
||||||
meshtastic_NodeInfoLite node;
|
|
||||||
auto *vec = static_cast<std::vector<meshtastic_NodeInfoLite> *>(iter->pData);
|
|
||||||
if (istream->bytes_left && pb_decode(istream, meshtastic_NodeInfoLite_fields, &node))
|
|
||||||
vec->push_back(node);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
case meshtastic_NodeDatabase_positions_tag: {
|
if (istream) {
|
||||||
if (ostream) {
|
meshtastic_NodeInfoLite node; // this gets good data
|
||||||
const auto *vec = static_cast<const std::vector<meshtastic_NodePositionEntry> *>(iter->pData);
|
std::vector<meshtastic_NodeInfoLite> *vec = (std::vector<meshtastic_NodeInfoLite> *)field->pData;
|
||||||
for (auto item : *vec) {
|
|
||||||
if (!pb_encode_tag_for_field(ostream, iter))
|
if (istream->bytes_left && pb_decode(istream, meshtastic_NodeInfoLite_fields, &node))
|
||||||
return false;
|
vec->push_back(node);
|
||||||
if (!pb_encode_submessage(ostream, meshtastic_NodePositionEntry_fields, &item))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (istream) {
|
|
||||||
meshtastic_NodePositionEntry entry;
|
|
||||||
auto *vec = static_cast<std::vector<meshtastic_NodePositionEntry> *>(iter->pData);
|
|
||||||
if (istream->bytes_left && pb_decode(istream, meshtastic_NodePositionEntry_fields, &entry))
|
|
||||||
vec->push_back(entry);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case meshtastic_NodeDatabase_telemetry_tag: {
|
|
||||||
if (ostream) {
|
|
||||||
const auto *vec = static_cast<const std::vector<meshtastic_NodeTelemetryEntry> *>(iter->pData);
|
|
||||||
for (auto item : *vec) {
|
|
||||||
if (!pb_encode_tag_for_field(ostream, iter))
|
|
||||||
return false;
|
|
||||||
if (!pb_encode_submessage(ostream, meshtastic_NodeTelemetryEntry_fields, &item))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (istream) {
|
|
||||||
meshtastic_NodeTelemetryEntry entry;
|
|
||||||
auto *vec = static_cast<std::vector<meshtastic_NodeTelemetryEntry> *>(iter->pData);
|
|
||||||
if (istream->bytes_left && pb_decode(istream, meshtastic_NodeTelemetryEntry_fields, &entry))
|
|
||||||
vec->push_back(entry);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case meshtastic_NodeDatabase_status_tag: {
|
|
||||||
if (ostream) {
|
|
||||||
const auto *vec = static_cast<const std::vector<meshtastic_NodeStatusEntry> *>(iter->pData);
|
|
||||||
for (auto item : *vec) {
|
|
||||||
if (!pb_encode_tag_for_field(ostream, iter))
|
|
||||||
return false;
|
|
||||||
if (!pb_encode_submessage(ostream, meshtastic_NodeStatusEntry_fields, &item))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (istream) {
|
|
||||||
meshtastic_NodeStatusEntry entry;
|
|
||||||
auto *vec = static_cast<std::vector<meshtastic_NodeStatusEntry> *>(iter->pData);
|
|
||||||
if (istream->bytes_left && pb_decode(istream, meshtastic_NodeStatusEntry_fields, &entry))
|
|
||||||
vec->push_back(entry);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case meshtastic_NodeDatabase_environment_tag: {
|
|
||||||
if (ostream) {
|
|
||||||
const auto *vec = static_cast<const std::vector<meshtastic_NodeEnvironmentEntry> *>(iter->pData);
|
|
||||||
for (auto item : *vec) {
|
|
||||||
if (!pb_encode_tag_for_field(ostream, iter))
|
|
||||||
return false;
|
|
||||||
if (!pb_encode_submessage(ostream, meshtastic_NodeEnvironmentEntry_fields, &item))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (istream) {
|
|
||||||
meshtastic_NodeEnvironmentEntry entry;
|
|
||||||
auto *vec = static_cast<std::vector<meshtastic_NodeEnvironmentEntry> *>(iter->pData);
|
|
||||||
if (istream->bytes_left && pb_decode(istream, meshtastic_NodeEnvironmentEntry_fields, &entry))
|
|
||||||
vec->push_back(entry);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The current change # for radio settings. Starts at 0 on boot and any time the radio settings
|
/** The current change # for radio settings. Starts at 0 on boot and any time the radio settings
|
||||||
@@ -378,7 +298,8 @@ NodeDB::NodeDB()
|
|||||||
#endif
|
#endif
|
||||||
// Include our owner in the node db under our nodenum
|
// Include our owner in the node db under our nodenum
|
||||||
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(getNodeNum());
|
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(getNodeNum());
|
||||||
TypeConversions::CopyUserToNodeInfoLite(info, owner);
|
info->user = TypeConversions::ConvertToUserLite(owner);
|
||||||
|
info->has_user = true;
|
||||||
|
|
||||||
// If node database has not been saved for the first time, save it now
|
// If node database has not been saved for the first time, save it now
|
||||||
#ifdef FSCom
|
#ifdef FSCom
|
||||||
@@ -521,12 +442,8 @@ NodeDB::NodeDB()
|
|||||||
#if defined(USERPREFS_FIXED_GPS_LAT) && defined(USERPREFS_FIXED_GPS_LON)
|
#if defined(USERPREFS_FIXED_GPS_LAT) && defined(USERPREFS_FIXED_GPS_LON)
|
||||||
fixedGPS.location_source = meshtastic_Position_LocSource_LOC_MANUAL;
|
fixedGPS.location_source = meshtastic_Position_LocSource_LOC_MANUAL;
|
||||||
config.has_position = true;
|
config.has_position = true;
|
||||||
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
info->has_position = true;
|
||||||
{
|
info->position = TypeConversions::ConvertToPositionLite(fixedGPS);
|
||||||
concurrency::LockGuard guard(&satelliteMutex);
|
|
||||||
nodePositions[info->num] = TypeConversions::ConvertToPositionLite(fixedGPS);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
nodeDB->setLocalPosition(fixedGPS);
|
nodeDB->setLocalPosition(fixedGPS);
|
||||||
config.position.fixed_position = true;
|
config.position.fixed_position = true;
|
||||||
#endif
|
#endif
|
||||||
@@ -562,29 +479,6 @@ bool isBroadcast(uint32_t dest)
|
|||||||
return dest == NODENUM_BROADCAST || dest == NODENUM_BROADCAST_NO_LORA;
|
return dest == NODENUM_BROADCAST || dest == NODENUM_BROADCAST_NO_LORA;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
template <typename Map, typename Value> bool copySatelliteEntry(const Map &map, NodeNum n, Value &out)
|
|
||||||
{
|
|
||||||
auto it = map.find(n);
|
|
||||||
if (it == map.end())
|
|
||||||
return false;
|
|
||||||
out = it->second;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename Map> std::vector<NodeNum> snapshotSatelliteNodeNums(const Map &map, NodeNum exclude)
|
|
||||||
{
|
|
||||||
std::vector<NodeNum> result;
|
|
||||||
result.reserve(map.size());
|
|
||||||
for (const auto &kv : map) {
|
|
||||||
if (kv.first != exclude)
|
|
||||||
result.push_back(kv.first);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
void NodeDB::resetRadioConfig(bool is_fresh_install)
|
void NodeDB::resetRadioConfig(bool is_fresh_install)
|
||||||
{
|
{
|
||||||
if (is_fresh_install) {
|
if (is_fresh_install) {
|
||||||
@@ -654,19 +548,6 @@ void NodeDB::installDefaultNodeDatabase()
|
|||||||
nodeDatabase.nodes = std::vector<meshtastic_NodeInfoLite>(MAX_NUM_NODES);
|
nodeDatabase.nodes = std::vector<meshtastic_NodeInfoLite>(MAX_NUM_NODES);
|
||||||
numMeshNodes = 0;
|
numMeshNodes = 0;
|
||||||
meshNodes = &nodeDatabase.nodes;
|
meshNodes = &nodeDatabase.nodes;
|
||||||
concurrency::LockGuard satelliteGuard(&satelliteMutex);
|
|
||||||
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
|
||||||
nodePositions.clear();
|
|
||||||
#endif
|
|
||||||
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
|
||||||
nodeTelemetry.clear();
|
|
||||||
#endif
|
|
||||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
|
||||||
nodeEnvironment.clear();
|
|
||||||
#endif
|
|
||||||
#if !MESHTASTIC_EXCLUDE_STATUSDB
|
|
||||||
nodeStatus.clear();
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void NodeDB::installDefaultConfig(bool preserveKey = false)
|
void NodeDB::installDefaultConfig(bool preserveKey = false)
|
||||||
@@ -1138,14 +1019,12 @@ void NodeDB::resetNodes(bool keepFavorites)
|
|||||||
{
|
{
|
||||||
if (!config.position.fixed_position)
|
if (!config.position.fixed_position)
|
||||||
clearLocalPosition();
|
clearLocalPosition();
|
||||||
NodeNum ourNum = getNodeNum();
|
|
||||||
numMeshNodes = 1;
|
numMeshNodes = 1;
|
||||||
if (keepFavorites) {
|
if (keepFavorites) {
|
||||||
LOG_INFO("Clearing node database - preserving favorites");
|
LOG_INFO("Clearing node database - preserving favorites");
|
||||||
for (size_t i = 0; i < meshNodes->size(); i++) {
|
for (size_t i = 0; i < meshNodes->size(); i++) {
|
||||||
meshtastic_NodeInfoLite &node = meshNodes->at(i);
|
meshtastic_NodeInfoLite &node = meshNodes->at(i);
|
||||||
if (i > 0 && !nodeInfoLiteIsFavorite(&node)) {
|
if (i > 0 && !node.is_favorite) {
|
||||||
eraseNodeSatellites(node.num);
|
|
||||||
node = meshtastic_NodeInfoLite();
|
node = meshtastic_NodeInfoLite();
|
||||||
} else {
|
} else {
|
||||||
numMeshNodes += 1;
|
numMeshNodes += 1;
|
||||||
@@ -1153,14 +1032,8 @@ void NodeDB::resetNodes(bool keepFavorites)
|
|||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
LOG_INFO("Clearing node database - removing favorites");
|
LOG_INFO("Clearing node database - removing favorites");
|
||||||
for (size_t i = 1; i < meshNodes->size(); i++) {
|
|
||||||
const NodeNum gone = meshNodes->at(i).num;
|
|
||||||
if (gone)
|
|
||||||
eraseNodeSatellites(gone);
|
|
||||||
}
|
|
||||||
std::fill(nodeDatabase.nodes.begin() + 1, nodeDatabase.nodes.end(), meshtastic_NodeInfoLite());
|
std::fill(nodeDatabase.nodes.begin() + 1, nodeDatabase.nodes.end(), meshtastic_NodeInfoLite());
|
||||||
}
|
}
|
||||||
(void)ourNum;
|
|
||||||
devicestate.has_rx_text_message = false;
|
devicestate.has_rx_text_message = false;
|
||||||
devicestate.has_rx_waypoint = false;
|
devicestate.has_rx_waypoint = false;
|
||||||
saveNodeDatabaseToDisk();
|
saveNodeDatabaseToDisk();
|
||||||
@@ -1181,173 +1054,36 @@ void NodeDB::removeNodeByNum(NodeNum nodeNum)
|
|||||||
numMeshNodes -= removed;
|
numMeshNodes -= removed;
|
||||||
std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.begin() + numMeshNodes + 1,
|
std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.begin() + numMeshNodes + 1,
|
||||||
meshtastic_NodeInfoLite());
|
meshtastic_NodeInfoLite());
|
||||||
if (removed)
|
|
||||||
eraseNodeSatellites(nodeNum);
|
|
||||||
LOG_DEBUG("NodeDB::removeNodeByNum purged %d entries. Save changes", removed);
|
LOG_DEBUG("NodeDB::removeNodeByNum purged %d entries. Save changes", removed);
|
||||||
saveNodeDatabaseToDisk();
|
saveNodeDatabaseToDisk();
|
||||||
}
|
}
|
||||||
|
|
||||||
void NodeDB::clearLocalPosition()
|
void NodeDB::clearLocalPosition()
|
||||||
{
|
{
|
||||||
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
meshtastic_NodeInfoLite *node = getMeshNode(nodeDB->getNodeNum());
|
||||||
concurrency::LockGuard guard(&satelliteMutex);
|
node->position.latitude_i = 0;
|
||||||
nodePositions.erase(getNodeNum());
|
node->position.longitude_i = 0;
|
||||||
#endif
|
node->position.altitude = 0;
|
||||||
|
node->position.time = 0;
|
||||||
setLocalPosition(meshtastic_Position_init_default);
|
setLocalPosition(meshtastic_Position_init_default);
|
||||||
localPositionUpdatedSinceBoot = false;
|
localPositionUpdatedSinceBoot = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool NodeDB::copyNodePosition(NodeNum n, meshtastic_PositionLite &out) const
|
|
||||||
{
|
|
||||||
#if MESHTASTIC_EXCLUDE_POSITIONDB
|
|
||||||
(void)n;
|
|
||||||
(void)out;
|
|
||||||
return false;
|
|
||||||
#else
|
|
||||||
concurrency::LockGuard guard(&satelliteMutex);
|
|
||||||
return copySatelliteEntry(nodePositions, n, out);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
bool NodeDB::copyNodeTelemetry(NodeNum n, meshtastic_DeviceMetrics &out) const
|
|
||||||
{
|
|
||||||
#if MESHTASTIC_EXCLUDE_TELEMETRYDB
|
|
||||||
(void)n;
|
|
||||||
(void)out;
|
|
||||||
return false;
|
|
||||||
#else
|
|
||||||
concurrency::LockGuard guard(&satelliteMutex);
|
|
||||||
return copySatelliteEntry(nodeTelemetry, n, out);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
bool NodeDB::copyNodeEnvironment(NodeNum n, meshtastic_EnvironmentMetrics &out) const
|
|
||||||
{
|
|
||||||
#if MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
|
||||||
(void)n;
|
|
||||||
(void)out;
|
|
||||||
return false;
|
|
||||||
#else
|
|
||||||
concurrency::LockGuard guard(&satelliteMutex);
|
|
||||||
return copySatelliteEntry(nodeEnvironment, n, out);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
bool NodeDB::copyNodeStatus(NodeNum n, meshtastic_StatusMessage &out) const
|
|
||||||
{
|
|
||||||
#if MESHTASTIC_EXCLUDE_STATUSDB
|
|
||||||
(void)n;
|
|
||||||
(void)out;
|
|
||||||
return false;
|
|
||||||
#else
|
|
||||||
concurrency::LockGuard guard(&satelliteMutex);
|
|
||||||
return copySatelliteEntry(nodeStatus, n, out);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<NodeNum> NodeDB::snapshotPositionNodeNums(NodeNum exclude) const
|
|
||||||
{
|
|
||||||
#if MESHTASTIC_EXCLUDE_POSITIONDB
|
|
||||||
(void)exclude;
|
|
||||||
return {};
|
|
||||||
#else
|
|
||||||
concurrency::LockGuard guard(&satelliteMutex);
|
|
||||||
return snapshotSatelliteNodeNums(nodePositions, exclude);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<NodeNum> NodeDB::snapshotTelemetryNodeNums(NodeNum exclude) const
|
|
||||||
{
|
|
||||||
#if MESHTASTIC_EXCLUDE_TELEMETRYDB
|
|
||||||
(void)exclude;
|
|
||||||
return {};
|
|
||||||
#else
|
|
||||||
concurrency::LockGuard guard(&satelliteMutex);
|
|
||||||
return snapshotSatelliteNodeNums(nodeTelemetry, exclude);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<NodeNum> NodeDB::snapshotEnvironmentNodeNums(NodeNum exclude) const
|
|
||||||
{
|
|
||||||
#if MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
|
||||||
(void)exclude;
|
|
||||||
return {};
|
|
||||||
#else
|
|
||||||
concurrency::LockGuard guard(&satelliteMutex);
|
|
||||||
return snapshotSatelliteNodeNums(nodeEnvironment, exclude);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<NodeNum> NodeDB::snapshotStatusNodeNums(NodeNum exclude) const
|
|
||||||
{
|
|
||||||
#if MESHTASTIC_EXCLUDE_STATUSDB
|
|
||||||
(void)exclude;
|
|
||||||
return {};
|
|
||||||
#else
|
|
||||||
concurrency::LockGuard guard(&satelliteMutex);
|
|
||||||
return snapshotSatelliteNodeNums(nodeStatus, exclude);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
void NodeDB::setNodeStatus(NodeNum n, const meshtastic_StatusMessage &status)
|
|
||||||
{
|
|
||||||
#if MESHTASTIC_EXCLUDE_STATUSDB
|
|
||||||
(void)n;
|
|
||||||
(void)status;
|
|
||||||
#else
|
|
||||||
concurrency::LockGuard guard(&satelliteMutex);
|
|
||||||
nodeStatus[n] = status;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
void NodeDB::touchNodePositionTime(NodeNum n, uint32_t time)
|
|
||||||
{
|
|
||||||
#if MESHTASTIC_EXCLUDE_POSITIONDB
|
|
||||||
(void)n;
|
|
||||||
(void)time;
|
|
||||||
#else
|
|
||||||
concurrency::LockGuard guard(&satelliteMutex);
|
|
||||||
nodePositions[n].time = time;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
void NodeDB::eraseNodeSatellites(NodeNum n)
|
|
||||||
{
|
|
||||||
concurrency::LockGuard guard(&satelliteMutex);
|
|
||||||
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
|
||||||
nodePositions.erase(n);
|
|
||||||
#endif
|
|
||||||
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
|
||||||
nodeTelemetry.erase(n);
|
|
||||||
#endif
|
|
||||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
|
||||||
nodeEnvironment.erase(n);
|
|
||||||
#endif
|
|
||||||
#if !MESHTASTIC_EXCLUDE_STATUSDB
|
|
||||||
nodeStatus.erase(n);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
void NodeDB::cleanupMeshDB()
|
void NodeDB::cleanupMeshDB()
|
||||||
{
|
{
|
||||||
int newPos = 0, removed = 0;
|
int newPos = 0, removed = 0;
|
||||||
for (int i = 0; i < numMeshNodes; i++) {
|
for (int i = 0; i < numMeshNodes; i++) {
|
||||||
meshtastic_NodeInfoLite &n = meshNodes->at(i);
|
if (meshNodes->at(i).has_user) {
|
||||||
if (nodeInfoLiteHasUser(&n)) {
|
if (meshNodes->at(i).user.public_key.size > 0) {
|
||||||
if (n.public_key.size > 0) {
|
if (memfll(meshNodes->at(i).user.public_key.bytes, 0, meshNodes->at(i).user.public_key.size)) {
|
||||||
if (memfll(n.public_key.bytes, 0, n.public_key.size)) {
|
meshNodes->at(i).user.public_key.size = 0;
|
||||||
n.public_key.size = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (newPos != i)
|
if (newPos != i)
|
||||||
meshNodes->at(newPos++) = n;
|
meshNodes->at(newPos++) = meshNodes->at(i);
|
||||||
else
|
else
|
||||||
newPos++;
|
newPos++;
|
||||||
} else {
|
} else {
|
||||||
// No user info - drop this node and its satellites
|
|
||||||
const NodeNum gone = n.num;
|
|
||||||
if (gone)
|
|
||||||
eraseNodeSatellites(gone);
|
|
||||||
removed++;
|
removed++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1405,22 +1141,14 @@ void NodeDB::pickNewNodeNum()
|
|||||||
nodeNum = (ourMacAddr[2] << 24) | (ourMacAddr[3] << 16) | (ourMacAddr[4] << 8) | ourMacAddr[5];
|
nodeNum = (ourMacAddr[2] << 24) | (ourMacAddr[3] << 16) | (ourMacAddr[4] << 8) | ourMacAddr[5];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Identity check via public key (or "empty slot?" when no keys yet);
|
|
||||||
// macaddr no longer lives on the slim header.
|
|
||||||
auto isOurOwnEntry = [&](const meshtastic_NodeInfoLite *n) -> bool {
|
|
||||||
if (!n)
|
|
||||||
return false;
|
|
||||||
if (owner.public_key.size == 32 && n->public_key.size == 32)
|
|
||||||
return memcmp(n->public_key.bytes, owner.public_key.bytes, 32) == 0;
|
|
||||||
return !nodeInfoLiteHasUser(n);
|
|
||||||
};
|
|
||||||
|
|
||||||
meshtastic_NodeInfoLite *found;
|
meshtastic_NodeInfoLite *found;
|
||||||
while (((found = getMeshNode(nodeNum)) && !isOurOwnEntry(found)) ||
|
while (((found = getMeshNode(nodeNum)) && memcmp(found->user.macaddr, ourMacAddr, sizeof(ourMacAddr)) != 0) ||
|
||||||
(nodeNum == NODENUM_BROADCAST || nodeNum < NUM_RESERVED)) {
|
(nodeNum == NODENUM_BROADCAST || nodeNum < NUM_RESERVED)) {
|
||||||
NodeNum candidate = random(NUM_RESERVED, LONG_MAX); // try a new random choice
|
NodeNum candidate = random(NUM_RESERVED, LONG_MAX); // try a new random choice
|
||||||
if (found)
|
if (found)
|
||||||
LOG_WARN("NOTE! Our desired nodenum 0x%x is invalid or in use, picking 0x%x", nodeNum, candidate);
|
LOG_WARN("NOTE! Our desired nodenum 0x%x is invalid or in use, by MAC ending in 0x%02x%02x vs our 0x%02x%02x, so "
|
||||||
|
"trying for 0x%x",
|
||||||
|
nodeNum, found->user.macaddr[4], found->user.macaddr[5], ourMacAddr[4], ourMacAddr[5], candidate);
|
||||||
nodeNum = candidate;
|
nodeNum = candidate;
|
||||||
}
|
}
|
||||||
LOG_DEBUG("Use nodenum 0x%x ", nodeNum);
|
LOG_DEBUG("Use nodenum 0x%x ", nodeNum);
|
||||||
@@ -1441,8 +1169,7 @@ LoadFileResult NodeDB::loadProto(const char *filename, size_t protoSize, size_t
|
|||||||
if (f) {
|
if (f) {
|
||||||
LOG_INFO("Load %s", filename);
|
LOG_INFO("Load %s", filename);
|
||||||
pb_istream_t stream = {&readcb, &f, protoSize};
|
pb_istream_t stream = {&readcb, &f, protoSize};
|
||||||
if (fields != &meshtastic_NodeDatabase_msg &&
|
if (fields != &meshtastic_NodeDatabase_msg) // contains a vector object
|
||||||
fields != &meshtastic_NodeDatabase_Legacy_msg) // both NodeDatabase descriptors contain std::vector members
|
|
||||||
memset(dest_struct, 0, objSize);
|
memset(dest_struct, 0, objSize);
|
||||||
if (!pb_decode(&stream, fields, dest_struct)) {
|
if (!pb_decode(&stream, fields, dest_struct)) {
|
||||||
LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream));
|
LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream));
|
||||||
@@ -1515,57 +1242,9 @@ void NodeDB::loadFromDisk()
|
|||||||
if (nodeDatabase.version < DEVICESTATE_MIN_VER) {
|
if (nodeDatabase.version < DEVICESTATE_MIN_VER) {
|
||||||
LOG_WARN("NodeDatabase %d is old, discard", nodeDatabase.version);
|
LOG_WARN("NodeDatabase %d is old, discard", nodeDatabase.version);
|
||||||
installDefaultNodeDatabase();
|
installDefaultNodeDatabase();
|
||||||
} else if (nodeDatabase.version < DEVICESTATE_CUR_VER) {
|
|
||||||
if (migrateLegacyNodeDatabase())
|
|
||||||
saveNodeDatabaseToDisk();
|
|
||||||
else
|
|
||||||
installDefaultNodeDatabase();
|
|
||||||
} else {
|
} else {
|
||||||
meshNodes = &nodeDatabase.nodes;
|
meshNodes = &nodeDatabase.nodes;
|
||||||
numMeshNodes = nodeDatabase.nodes.size();
|
numMeshNodes = nodeDatabase.nodes.size();
|
||||||
// Hydrate the satellite maps; the on-disk vectors stay empty in steady
|
|
||||||
// state and are repopulated only at save time.
|
|
||||||
concurrency::LockGuard guard(&satelliteMutex);
|
|
||||||
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
|
||||||
nodePositions.clear();
|
|
||||||
nodePositions.reserve(nodeDatabase.positions.size());
|
|
||||||
for (const auto &entry : nodeDatabase.positions) {
|
|
||||||
if (entry.has_position)
|
|
||||||
nodePositions[entry.num] = entry.position;
|
|
||||||
}
|
|
||||||
nodeDatabase.positions.clear();
|
|
||||||
nodeDatabase.positions.shrink_to_fit();
|
|
||||||
#endif
|
|
||||||
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
|
||||||
nodeTelemetry.clear();
|
|
||||||
nodeTelemetry.reserve(nodeDatabase.telemetry.size());
|
|
||||||
for (const auto &entry : nodeDatabase.telemetry) {
|
|
||||||
if (entry.has_device_metrics)
|
|
||||||
nodeTelemetry[entry.num] = entry.device_metrics;
|
|
||||||
}
|
|
||||||
nodeDatabase.telemetry.clear();
|
|
||||||
nodeDatabase.telemetry.shrink_to_fit();
|
|
||||||
#endif
|
|
||||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
|
||||||
nodeEnvironment.clear();
|
|
||||||
nodeEnvironment.reserve(nodeDatabase.environment.size());
|
|
||||||
for (const auto &entry : nodeDatabase.environment) {
|
|
||||||
if (entry.has_environment_metrics)
|
|
||||||
nodeEnvironment[entry.num] = entry.environment_metrics;
|
|
||||||
}
|
|
||||||
nodeDatabase.environment.clear();
|
|
||||||
nodeDatabase.environment.shrink_to_fit();
|
|
||||||
#endif
|
|
||||||
#if !MESHTASTIC_EXCLUDE_STATUSDB
|
|
||||||
nodeStatus.clear();
|
|
||||||
nodeStatus.reserve(nodeDatabase.status.size());
|
|
||||||
for (const auto &entry : nodeDatabase.status) {
|
|
||||||
if (entry.has_status)
|
|
||||||
nodeStatus[entry.num] = entry.status;
|
|
||||||
}
|
|
||||||
nodeDatabase.status.clear();
|
|
||||||
nodeDatabase.status.shrink_to_fit();
|
|
||||||
#endif
|
|
||||||
LOG_INFO("Loaded saved nodedatabase version %d, with nodes count: %d", nodeDatabase.version, nodeDatabase.nodes.size());
|
LOG_INFO("Loaded saved nodedatabase version %d, with nodes count: %d", nodeDatabase.version, nodeDatabase.nodes.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1593,16 +1272,16 @@ void NodeDB::loadFromDisk()
|
|||||||
|
|
||||||
// Attempt recovery of owner fields from our own NodeDB entry if available.
|
// Attempt recovery of owner fields from our own NodeDB entry if available.
|
||||||
meshtastic_NodeInfoLite *us = getMeshNode(getNodeNum());
|
meshtastic_NodeInfoLite *us = getMeshNode(getNodeNum());
|
||||||
if (nodeInfoLiteHasUser(us)) {
|
if (us && us->has_user) {
|
||||||
LOG_WARN("Restoring owner fields (long_name/short_name/is_licensed/is_unmessagable) from NodeDB for our node 0x%08x",
|
LOG_WARN("Restoring owner fields (long_name/short_name/is_licensed/is_unmessagable) from NodeDB for our node 0x%08x",
|
||||||
us->num);
|
us->num);
|
||||||
memcpy(owner.long_name, us->long_name, sizeof(owner.long_name));
|
memcpy(owner.long_name, us->user.long_name, sizeof(owner.long_name));
|
||||||
owner.long_name[sizeof(owner.long_name) - 1] = '\0';
|
owner.long_name[sizeof(owner.long_name) - 1] = '\0';
|
||||||
memcpy(owner.short_name, us->short_name, sizeof(owner.short_name));
|
memcpy(owner.short_name, us->user.short_name, sizeof(owner.short_name));
|
||||||
owner.short_name[sizeof(owner.short_name) - 1] = '\0';
|
owner.short_name[sizeof(owner.short_name) - 1] = '\0';
|
||||||
owner.is_licensed = nodeInfoLiteIsLicensed(us);
|
owner.is_licensed = us->user.is_licensed;
|
||||||
owner.has_is_unmessagable = nodeInfoLiteHasIsUnmessagable(us);
|
owner.has_is_unmessagable = us->user.has_is_unmessagable;
|
||||||
owner.is_unmessagable = nodeInfoLiteIsUnmessagable(us);
|
owner.is_unmessagable = us->user.is_unmessagable;
|
||||||
|
|
||||||
// Save the recovered owner to device state on disk
|
// Save the recovered owner to device state on disk
|
||||||
saveToDisk(SEGMENT_DEVICESTATE);
|
saveToDisk(SEGMENT_DEVICESTATE);
|
||||||
@@ -1849,75 +1528,9 @@ bool NodeDB::saveNodeDatabaseToDisk()
|
|||||||
FSCom.mkdir("/prefs");
|
FSCom.mkdir("/prefs");
|
||||||
spiLock->unlock();
|
spiLock->unlock();
|
||||||
#endif
|
#endif
|
||||||
// Project the maps into the on-disk vectors just before encoding; cleared
|
|
||||||
// again on the way out so we don't carry duplicate state.
|
|
||||||
concurrency::LockGuard guard(&satelliteMutex);
|
|
||||||
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
|
||||||
nodeDatabase.positions.clear();
|
|
||||||
nodeDatabase.positions.reserve(nodePositions.size());
|
|
||||||
for (const auto &kv : nodePositions) {
|
|
||||||
meshtastic_NodePositionEntry entry = meshtastic_NodePositionEntry_init_default;
|
|
||||||
entry.num = kv.first;
|
|
||||||
entry.has_position = true;
|
|
||||||
entry.position = kv.second;
|
|
||||||
nodeDatabase.positions.push_back(entry);
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
nodeDatabase.positions.clear();
|
|
||||||
#endif
|
|
||||||
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
|
||||||
nodeDatabase.telemetry.clear();
|
|
||||||
nodeDatabase.telemetry.reserve(nodeTelemetry.size());
|
|
||||||
for (const auto &kv : nodeTelemetry) {
|
|
||||||
meshtastic_NodeTelemetryEntry entry = meshtastic_NodeTelemetryEntry_init_default;
|
|
||||||
entry.num = kv.first;
|
|
||||||
entry.has_device_metrics = true;
|
|
||||||
entry.device_metrics = kv.second;
|
|
||||||
nodeDatabase.telemetry.push_back(entry);
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
nodeDatabase.telemetry.clear();
|
|
||||||
#endif
|
|
||||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
|
||||||
nodeDatabase.environment.clear();
|
|
||||||
nodeDatabase.environment.reserve(nodeEnvironment.size());
|
|
||||||
for (const auto &kv : nodeEnvironment) {
|
|
||||||
meshtastic_NodeEnvironmentEntry entry = meshtastic_NodeEnvironmentEntry_init_default;
|
|
||||||
entry.num = kv.first;
|
|
||||||
entry.has_environment_metrics = true;
|
|
||||||
entry.environment_metrics = kv.second;
|
|
||||||
nodeDatabase.environment.push_back(entry);
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
nodeDatabase.environment.clear();
|
|
||||||
#endif
|
|
||||||
#if !MESHTASTIC_EXCLUDE_STATUSDB
|
|
||||||
nodeDatabase.status.clear();
|
|
||||||
nodeDatabase.status.reserve(nodeStatus.size());
|
|
||||||
for (const auto &kv : nodeStatus) {
|
|
||||||
meshtastic_NodeStatusEntry entry = meshtastic_NodeStatusEntry_init_default;
|
|
||||||
entry.num = kv.first;
|
|
||||||
entry.has_status = true;
|
|
||||||
entry.status = kv.second;
|
|
||||||
nodeDatabase.status.push_back(entry);
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
nodeDatabase.status.clear();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
size_t nodeDatabaseSize;
|
size_t nodeDatabaseSize;
|
||||||
pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &nodeDatabase);
|
pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &nodeDatabase);
|
||||||
bool ok = saveProto(nodeDatabaseFileName, nodeDatabaseSize, &meshtastic_NodeDatabase_msg, &nodeDatabase, false);
|
return saveProto(nodeDatabaseFileName, nodeDatabaseSize, &meshtastic_NodeDatabase_msg, &nodeDatabase, false);
|
||||||
|
|
||||||
nodeDatabase.positions.clear();
|
|
||||||
nodeDatabase.positions.shrink_to_fit();
|
|
||||||
nodeDatabase.telemetry.clear();
|
|
||||||
nodeDatabase.telemetry.shrink_to_fit();
|
|
||||||
nodeDatabase.environment.clear();
|
|
||||||
nodeDatabase.environment.shrink_to_fit();
|
|
||||||
nodeDatabase.status.clear();
|
|
||||||
nodeDatabase.status.shrink_to_fit();
|
|
||||||
return ok;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool NodeDB::saveToDiskNoRetry(int saveWhat)
|
bool NodeDB::saveToDiskNoRetry(int saveWhat)
|
||||||
@@ -1998,10 +1611,12 @@ bool NodeDB::saveToDisk(int saveWhat)
|
|||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
LOG_ERROR("Failed to save to disk, retrying");
|
LOG_ERROR("Failed to save to disk, retrying");
|
||||||
|
#ifdef ARCH_NRF52 // @geeksville is not ready yet to say we should do this on other platforms. See bug #4184 discussion
|
||||||
spiLock->lock();
|
spiLock->lock();
|
||||||
fsFormat();
|
FSCom.format();
|
||||||
spiLock->unlock();
|
spiLock->unlock();
|
||||||
|
|
||||||
|
#endif
|
||||||
success = saveToDiskNoRetry(saveWhat);
|
success = saveToDiskNoRetry(saveWhat);
|
||||||
|
|
||||||
RECORD_CRITICALERROR(success ? meshtastic_CriticalErrorCode_FLASH_CORRUPTION_RECOVERABLE
|
RECORD_CRITICALERROR(success ? meshtastic_CriticalErrorCode_FLASH_CORRUPTION_RECOVERABLE
|
||||||
@@ -2086,7 +1701,7 @@ size_t NodeDB::getNumOnlineMeshNodes(bool localOnly)
|
|||||||
|
|
||||||
// FIXME this implementation is kinda expensive
|
// FIXME this implementation is kinda expensive
|
||||||
for (int i = 0; i < numMeshNodes; i++) {
|
for (int i = 0; i < numMeshNodes; i++) {
|
||||||
if (localOnly && nodeInfoLiteViaMqtt(&meshNodes->at(i)))
|
if (localOnly && meshNodes->at(i).via_mqtt)
|
||||||
continue;
|
continue;
|
||||||
if (sinceLastSeen(&meshNodes->at(i)) < NUM_ONLINE_SECS)
|
if (sinceLastSeen(&meshNodes->at(i)) < NUM_ONLINE_SECS)
|
||||||
numseen++;
|
numseen++;
|
||||||
@@ -2123,94 +1738,62 @@ void NodeDB::updatePosition(uint32_t nodeId, const meshtastic_Position &p, RxSou
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if MESHTASTIC_EXCLUDE_POSITIONDB
|
|
||||||
// Build flag opted out: header still tracks last_heard via updateFrom; we
|
|
||||||
// simply don't cache the position payload anywhere on this device.
|
|
||||||
if (src == RX_SRC_LOCAL) {
|
if (src == RX_SRC_LOCAL) {
|
||||||
LOG_INFO("updatePosition LOCAL (PositionDB excluded) time=%u lat=%d lon=%d alt=%d", p.time, p.latitude_i, p.longitude_i,
|
// Local packet, fully authoritative
|
||||||
|
LOG_INFO("updatePosition LOCAL pos@%x time=%u lat=%d lon=%d alt=%d", p.timestamp, p.time, p.latitude_i, p.longitude_i,
|
||||||
p.altitude);
|
p.altitude);
|
||||||
|
|
||||||
setLocalPosition(p);
|
setLocalPosition(p);
|
||||||
|
info->position = TypeConversions::ConvertToPositionLite(p);
|
||||||
|
} else if ((p.time > 0) && !p.latitude_i && !p.longitude_i && !p.timestamp && !p.location_source) {
|
||||||
|
// FIXME SPECIAL TIME SETTING PACKET FROM EUD TO RADIO
|
||||||
|
// (stop-gap fix for issue #900)
|
||||||
|
LOG_DEBUG("updatePosition SPECIAL time setting time=%u", p.time);
|
||||||
|
info->position.time = p.time;
|
||||||
|
} else {
|
||||||
|
// Be careful to only update fields that have been set by the REMOTE sender
|
||||||
|
// A lot of position reports don't have time populated. In that case, be careful to not blow away the time we
|
||||||
|
// recorded based on the packet rxTime
|
||||||
|
//
|
||||||
|
// FIXME perhaps handle RX_SRC_USER separately?
|
||||||
|
LOG_INFO("updatePosition REMOTE node=0x%x time=%u lat=%d lon=%d", nodeId, p.time, p.latitude_i, p.longitude_i);
|
||||||
|
|
||||||
|
// First, back up fields that we want to protect from overwrite
|
||||||
|
uint32_t tmp_time = info->position.time;
|
||||||
|
|
||||||
|
// Next, update atomically
|
||||||
|
info->position = TypeConversions::ConvertToPositionLite(p);
|
||||||
|
|
||||||
|
// Last, restore any fields that may have been overwritten
|
||||||
|
if (!info->position.time)
|
||||||
|
info->position.time = tmp_time;
|
||||||
}
|
}
|
||||||
(void)nodeId;
|
info->has_position = true;
|
||||||
updateGUIforNode = info;
|
|
||||||
notifyObservers(true);
|
|
||||||
#else
|
|
||||||
{
|
|
||||||
concurrency::LockGuard guard(&satelliteMutex);
|
|
||||||
meshtastic_PositionLite &slot = nodePositions[nodeId]; // creates default-zero entry if missing
|
|
||||||
|
|
||||||
if (src == RX_SRC_LOCAL) {
|
|
||||||
// Local packet, fully authoritative
|
|
||||||
LOG_INFO("updatePosition LOCAL pos@%x time=%u lat=%d lon=%d alt=%d", p.timestamp, p.time, p.latitude_i, p.longitude_i,
|
|
||||||
p.altitude);
|
|
||||||
|
|
||||||
setLocalPosition(p);
|
|
||||||
slot = TypeConversions::ConvertToPositionLite(p);
|
|
||||||
} else if ((p.time > 0) && !p.latitude_i && !p.longitude_i && !p.timestamp && !p.location_source) {
|
|
||||||
// FIXME SPECIAL TIME SETTING PACKET FROM EUD TO RADIO
|
|
||||||
// (stop-gap fix for issue #900)
|
|
||||||
LOG_DEBUG("updatePosition SPECIAL time setting time=%u", p.time);
|
|
||||||
slot.time = p.time;
|
|
||||||
} else {
|
|
||||||
// Be careful to only update fields that have been set by the REMOTE sender
|
|
||||||
// A lot of position reports don't have time populated. In that case, be careful to not blow away the time we
|
|
||||||
// recorded based on the packet rxTime
|
|
||||||
//
|
|
||||||
// FIXME perhaps handle RX_SRC_USER separately?
|
|
||||||
LOG_INFO("updatePosition REMOTE node=0x%x time=%u lat=%d lon=%d", nodeId, p.time, p.latitude_i, p.longitude_i);
|
|
||||||
|
|
||||||
// First, back up fields that we want to protect from overwrite
|
|
||||||
uint32_t tmp_time = slot.time;
|
|
||||||
|
|
||||||
// Next, update atomically
|
|
||||||
slot = TypeConversions::ConvertToPositionLite(p);
|
|
||||||
|
|
||||||
// Last, restore any fields that may have been overwritten
|
|
||||||
if (!slot.time)
|
|
||||||
slot.time = tmp_time;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updateGUIforNode = info;
|
updateGUIforNode = info;
|
||||||
notifyObservers(true); // Force an update whether or not our node counts have changed
|
notifyObservers(true); // Force an update whether or not our node counts have changed
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Update telemetry info for this node based on received metrics. Stores
|
/** Update telemetry info for this node based on received metrics
|
||||||
* device_metrics and environment_metrics into their respective satellite
|
* We only care about device telemetry here
|
||||||
* maps; other variants (air_quality, power, local_stats, health) are
|
|
||||||
* intentionally not retained per-node.
|
|
||||||
*/
|
*/
|
||||||
void NodeDB::updateTelemetry(uint32_t nodeId, const meshtastic_Telemetry &t, RxSource src)
|
void NodeDB::updateTelemetry(uint32_t nodeId, const meshtastic_Telemetry &t, RxSource src)
|
||||||
{
|
{
|
||||||
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(nodeId);
|
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(nodeId);
|
||||||
if (!info)
|
// Environment metrics should never go to NodeDb but we'll safegaurd anyway
|
||||||
|
if (!info || t.which_variant != meshtastic_Telemetry_device_metrics_tag) {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (t.which_variant == meshtastic_Telemetry_device_metrics_tag) {
|
|
||||||
if (src == RX_SRC_LOCAL) {
|
|
||||||
LOG_DEBUG("updateTelemetry LOCAL device");
|
|
||||||
} else {
|
|
||||||
LOG_DEBUG("updateTelemetry REMOTE device node=0x%x", nodeId);
|
|
||||||
}
|
|
||||||
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
|
||||||
concurrency::LockGuard guard(&satelliteMutex);
|
|
||||||
nodeTelemetry[nodeId] = t.variant.device_metrics;
|
|
||||||
#endif
|
|
||||||
} else if (t.which_variant == meshtastic_Telemetry_environment_metrics_tag) {
|
|
||||||
if (src == RX_SRC_LOCAL) {
|
|
||||||
LOG_DEBUG("updateTelemetry LOCAL env");
|
|
||||||
} else {
|
|
||||||
LOG_DEBUG("updateTelemetry REMOTE env node=0x%x", nodeId);
|
|
||||||
}
|
|
||||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
|
||||||
concurrency::LockGuard guard(&satelliteMutex);
|
|
||||||
nodeEnvironment[nodeId] = t.variant.environment_metrics;
|
|
||||||
#endif
|
|
||||||
} else {
|
|
||||||
return; // air_quality / power / local_stats / health: not stored per-node
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (src == RX_SRC_LOCAL) {
|
||||||
|
// Local packet, fully authoritative
|
||||||
|
LOG_DEBUG("updateTelemetry LOCAL");
|
||||||
|
} else {
|
||||||
|
LOG_DEBUG("updateTelemetry REMOTE node=0x%x ", nodeId);
|
||||||
|
}
|
||||||
|
info->device_metrics = t.variant.device_metrics;
|
||||||
|
info->has_device_metrics = true;
|
||||||
updateGUIforNode = info;
|
updateGUIforNode = info;
|
||||||
notifyObservers(true);
|
notifyObservers(true); // Force an update whether or not our node counts have changed
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2225,22 +1808,24 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact)
|
|||||||
// If the local node has this node marked as manually verified
|
// If the local node has this node marked as manually verified
|
||||||
// and the client does not, do not allow the client to update the
|
// and the client does not, do not allow the client to update the
|
||||||
// saved public key.
|
// saved public key.
|
||||||
if (nodeInfoLiteIsKeyManuallyVerified(info) && !contact.manually_verified) {
|
if ((info->bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) && !contact.manually_verified) {
|
||||||
if (contact.user.public_key.size != info->public_key.size ||
|
if (contact.user.public_key.size != info->user.public_key.size ||
|
||||||
memcmp(contact.user.public_key.bytes, info->public_key.bytes, info->public_key.size) != 0) {
|
memcmp(contact.user.public_key.bytes, info->user.public_key.bytes, info->user.public_key.size) != 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
info->num = contact.node_num;
|
info->num = contact.node_num;
|
||||||
TypeConversions::CopyUserToNodeInfoLite(info, contact.user);
|
info->has_user = true;
|
||||||
|
info->user = TypeConversions::ConvertToUserLite(contact.user);
|
||||||
if (contact.should_ignore) {
|
if (contact.should_ignore) {
|
||||||
// If should_ignore is set,
|
// If should_ignore is set,
|
||||||
// we need to clear the public key and other cruft, in addition to setting the node as ignored
|
// we need to clear the public key and other cruft, in addition to setting the node as ignored
|
||||||
nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_IS_IGNORED_MASK, true);
|
info->is_ignored = true;
|
||||||
nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_IS_FAVORITE_MASK, false);
|
info->is_favorite = false;
|
||||||
eraseNodeSatellites(contact.node_num);
|
info->has_device_metrics = false;
|
||||||
info->public_key.size = 0;
|
info->has_position = false;
|
||||||
memset(info->public_key.bytes, 0, sizeof(info->public_key.bytes));
|
info->user.public_key.size = 0;
|
||||||
|
memset(info->user.public_key.bytes, 0, sizeof(info->user.public_key.bytes));
|
||||||
} else {
|
} else {
|
||||||
/* Clients are sending add_contact before every text message DM (because clients may hold a larger node database with
|
/* Clients are sending add_contact before every text message DM (because clients may hold a larger node database with
|
||||||
* public keys than the radio holds). However, we don't want to update last_heard just because we sent someone a DM!
|
* public keys than the radio holds). However, we don't want to update last_heard just because we sent someone a DM!
|
||||||
@@ -2259,12 +1844,12 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact)
|
|||||||
} else {
|
} else {
|
||||||
// Normal case: set is_favorite to prevent expiration.
|
// Normal case: set is_favorite to prevent expiration.
|
||||||
// last_heard will remain as-is (or remain 0 if this entry wasn't in the nodeDB).
|
// last_heard will remain as-is (or remain 0 if this entry wasn't in the nodeDB).
|
||||||
nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_IS_FAVORITE_MASK, true);
|
info->is_favorite = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// As the clients will begin sending the contact with DMs, we want to strictly check if the node is manually verified
|
// As the clients will begin sending the contact with DMs, we want to strictly check if the node is manually verified
|
||||||
if (contact.manually_verified) {
|
if (contact.manually_verified) {
|
||||||
nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK, true);
|
info->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||||
}
|
}
|
||||||
// Mark the node's key as manually verified to indicate trustworthiness.
|
// Mark the node's key as manually verified to indicate trustworthiness.
|
||||||
updateGUIforNode = info;
|
updateGUIforNode = info;
|
||||||
@@ -2298,15 +1883,15 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde
|
|||||||
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
||||||
cn->level = meshtastic_LogRecord_Level_WARNING;
|
cn->level = meshtastic_LogRecord_Level_WARNING;
|
||||||
cn->time = getValidTime(RTCQualityFromNet);
|
cn->time = getValidTime(RTCQualityFromNet);
|
||||||
snprintf(cn->message, sizeof(cn->message), warning, p.long_name);
|
sprintf(cn->message, warning, p.long_name);
|
||||||
service->sendClientNotification(cn);
|
service->sendClientNotification(cn);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (info->public_key.size == 32) { // if we have a key for this user already, don't overwrite with a new one
|
if (info->user.public_key.size == 32) { // if we have a key for this user already, don't overwrite with a new one
|
||||||
// if the key doesn't match, don't update nodeDB at all.
|
// if the key doesn't match, don't update nodeDB at all.
|
||||||
if (p.public_key.size != 32 || (memcmp(p.public_key.bytes, info->public_key.bytes, 32) != 0)) {
|
if (p.public_key.size != 32 || (memcmp(p.public_key.bytes, info->user.public_key.bytes, 32) != 0)) {
|
||||||
LOG_WARN("Public Key mismatch, dropping NodeInfo");
|
LOG_WARN("Public Key mismatch, dropping NodeInfo");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -2319,22 +1904,19 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde
|
|||||||
// Always ensure user.id is derived from nodeId, regardless of what was received
|
// Always ensure user.id is derived from nodeId, regardless of what was received
|
||||||
snprintf(p.id, sizeof(p.id), "!%08x", nodeId);
|
snprintf(p.id, sizeof(p.id), "!%08x", nodeId);
|
||||||
|
|
||||||
meshtastic_NodeInfoLite before = *info;
|
// Both of info->user and p start as filled with zero so I think this is okay
|
||||||
TypeConversions::CopyUserToNodeInfoLite(info, p);
|
auto lite = TypeConversions::ConvertToUserLite(p);
|
||||||
bool changed =
|
bool changed = memcmp(&info->user, &lite, sizeof(info->user)) || (info->channel != channelIndex);
|
||||||
(memcmp(before.long_name, info->long_name, sizeof(info->long_name)) != 0) ||
|
|
||||||
(memcmp(before.short_name, info->short_name, sizeof(info->short_name)) != 0) || (before.hw_model != info->hw_model) ||
|
|
||||||
(before.role != info->role) || (before.public_key.size != info->public_key.size) ||
|
|
||||||
(info->public_key.size > 0 && memcmp(before.public_key.bytes, info->public_key.bytes, info->public_key.size) != 0) ||
|
|
||||||
(before.bitfield != info->bitfield) || (info->channel != channelIndex);
|
|
||||||
|
|
||||||
if (info->public_key.size == 32) {
|
info->user = lite;
|
||||||
printBytes("Saved Pubkey: ", info->public_key.bytes, 32);
|
if (info->user.public_key.size == 32) {
|
||||||
|
printBytes("Saved Pubkey: ", info->user.public_key.bytes, 32);
|
||||||
}
|
}
|
||||||
if (nodeId != getNodeNum())
|
if (nodeId != getNodeNum())
|
||||||
info->channel = channelIndex; // Set channel we need to use to reach this node (but don't set our own channel)
|
info->channel = channelIndex; // Set channel we need to use to reach this node (but don't set our own channel)
|
||||||
LOG_DEBUG("Update changed=%d user %s/%s, id=0x%08x, channel=%d", changed, info->long_name, info->short_name, nodeId,
|
LOG_DEBUG("Update changed=%d user %s/%s, id=0x%08x, channel=%d", changed, info->user.long_name, info->user.short_name, nodeId,
|
||||||
info->channel);
|
info->channel);
|
||||||
|
info->has_user = true;
|
||||||
|
|
||||||
if (changed) {
|
if (changed) {
|
||||||
updateGUIforNode = info;
|
updateGUIforNode = info;
|
||||||
@@ -2376,8 +1958,7 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp)
|
|||||||
if (mp.rx_snr)
|
if (mp.rx_snr)
|
||||||
info->snr = mp.rx_snr; // keep the most recent SNR we received for this node.
|
info->snr = mp.rx_snr; // keep the most recent SNR we received for this node.
|
||||||
|
|
||||||
nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_VIA_MQTT_MASK,
|
info->via_mqtt = mp.via_mqtt; // Store if we received this packet via MQTT
|
||||||
mp.via_mqtt); // Store if we received this packet via MQTT
|
|
||||||
|
|
||||||
// If hopStart was set and there wasn't someone messing with the limit in the middle, add hopsAway
|
// If hopStart was set and there wasn't someone messing with the limit in the middle, add hopsAway
|
||||||
const int8_t hopsAway = getHopsAway(mp);
|
const int8_t hopsAway = getHopsAway(mp);
|
||||||
@@ -2392,8 +1973,8 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp)
|
|||||||
void NodeDB::set_favorite(bool is_favorite, uint32_t nodeId)
|
void NodeDB::set_favorite(bool is_favorite, uint32_t nodeId)
|
||||||
{
|
{
|
||||||
meshtastic_NodeInfoLite *lite = getMeshNode(nodeId);
|
meshtastic_NodeInfoLite *lite = getMeshNode(nodeId);
|
||||||
if (lite && nodeInfoLiteIsFavorite(lite) != is_favorite) {
|
if (lite && lite->is_favorite != is_favorite) {
|
||||||
nodeInfoLiteSetBit(lite, NODEINFO_BITFIELD_IS_FAVORITE_MASK, is_favorite);
|
lite->is_favorite = is_favorite;
|
||||||
sortMeshDB();
|
sortMeshDB();
|
||||||
saveNodeDatabaseToDisk();
|
saveNodeDatabaseToDisk();
|
||||||
}
|
}
|
||||||
@@ -2407,10 +1988,10 @@ bool NodeDB::isFavorite(uint32_t nodeId)
|
|||||||
if (nodeId == NODENUM_BROADCAST)
|
if (nodeId == NODENUM_BROADCAST)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
const meshtastic_NodeInfoLite *lite = getMeshNode(nodeId);
|
meshtastic_NodeInfoLite *lite = getMeshNode(nodeId);
|
||||||
|
|
||||||
if (lite) {
|
if (lite) {
|
||||||
return nodeInfoLiteIsFavorite(lite);
|
return lite->is_favorite;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -2435,14 +2016,14 @@ bool NodeDB::isFromOrToFavoritedNode(const meshtastic_MeshPacket &p)
|
|||||||
lite = &meshNodes->at(i);
|
lite = &meshNodes->at(i);
|
||||||
|
|
||||||
if (lite->num == p.from) {
|
if (lite->num == p.from) {
|
||||||
if (nodeInfoLiteIsFavorite(lite))
|
if (lite->is_favorite)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
seenFrom = true;
|
seenFrom = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lite->num == p.to) {
|
if (lite->num == p.to) {
|
||||||
if (nodeInfoLiteIsFavorite(lite))
|
if (lite->is_favorite)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
seenTo = true;
|
seenTo = true;
|
||||||
@@ -2478,10 +2059,10 @@ void NodeDB::sortMeshDB()
|
|||||||
// TODO: Look for at(i-1) also matching own node num, and throw the DB in the trash
|
// TODO: Look for at(i-1) also matching own node num, and throw the DB in the trash
|
||||||
std::swap(meshNodes->at(i), meshNodes->at(i - 1));
|
std::swap(meshNodes->at(i), meshNodes->at(i - 1));
|
||||||
changed = true;
|
changed = true;
|
||||||
} else if (nodeInfoLiteIsFavorite(&meshNodes->at(i)) && !nodeInfoLiteIsFavorite(&meshNodes->at(i - 1))) {
|
} else if (meshNodes->at(i).is_favorite && !meshNodes->at(i - 1).is_favorite) {
|
||||||
std::swap(meshNodes->at(i), meshNodes->at(i - 1));
|
std::swap(meshNodes->at(i), meshNodes->at(i - 1));
|
||||||
changed = true;
|
changed = true;
|
||||||
} else if (!nodeInfoLiteIsFavorite(&meshNodes->at(i)) && nodeInfoLiteIsFavorite(&meshNodes->at(i - 1))) {
|
} else if (!meshNodes->at(i).is_favorite && meshNodes->at(i - 1).is_favorite) {
|
||||||
// noop
|
// noop
|
||||||
} else if (meshNodes->at(i).last_heard > meshNodes->at(i - 1).last_heard) {
|
} else if (meshNodes->at(i).last_heard > meshNodes->at(i - 1).last_heard) {
|
||||||
std::swap(meshNodes->at(i), meshNodes->at(i - 1));
|
std::swap(meshNodes->at(i), meshNodes->at(i - 1));
|
||||||
@@ -2541,18 +2122,17 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
|
|||||||
int oldestIndex = -1;
|
int oldestIndex = -1;
|
||||||
int oldestBoringIndex = -1;
|
int oldestBoringIndex = -1;
|
||||||
for (int i = 1; i < numMeshNodes; i++) {
|
for (int i = 1; i < numMeshNodes; i++) {
|
||||||
const meshtastic_NodeInfoLite *cand = &meshNodes->at(i);
|
|
||||||
const bool isFavoriteNode = nodeInfoLiteIsFavorite(cand);
|
|
||||||
const bool isIgnored = nodeInfoLiteIsIgnored(cand);
|
|
||||||
const bool isVerified = nodeInfoLiteIsKeyManuallyVerified(cand);
|
|
||||||
// Simply the oldest non-favorite, non-ignored, non-verified node
|
// Simply the oldest non-favorite, non-ignored, non-verified node
|
||||||
if (!isFavoriteNode && !isIgnored && !isVerified && cand->last_heard < oldest) {
|
if (!meshNodes->at(i).is_favorite && !meshNodes->at(i).is_ignored &&
|
||||||
oldest = cand->last_heard;
|
!(meshNodes->at(i).bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) &&
|
||||||
|
meshNodes->at(i).last_heard < oldest) {
|
||||||
|
oldest = meshNodes->at(i).last_heard;
|
||||||
oldestIndex = i;
|
oldestIndex = i;
|
||||||
}
|
}
|
||||||
// The oldest "boring" node
|
// The oldest "boring" node
|
||||||
if (!isFavoriteNode && !isIgnored && cand->public_key.size == 0 && cand->last_heard < oldestBoring) {
|
if (!meshNodes->at(i).is_favorite && !meshNodes->at(i).is_ignored && meshNodes->at(i).user.public_key.size == 0 &&
|
||||||
oldestBoring = cand->last_heard;
|
meshNodes->at(i).last_heard < oldestBoring) {
|
||||||
|
oldestBoring = meshNodes->at(i).last_heard;
|
||||||
oldestBoringIndex = i;
|
oldestBoringIndex = i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2562,7 +2142,6 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (oldestIndex != -1) {
|
if (oldestIndex != -1) {
|
||||||
eraseNodeSatellites(meshNodes->at(oldestIndex).num);
|
|
||||||
// Shove the remaining nodes down the chain
|
// Shove the remaining nodes down the chain
|
||||||
for (int i = oldestIndex; i < numMeshNodes - 1; i++) {
|
for (int i = oldestIndex; i < numMeshNodes - 1; i++) {
|
||||||
meshNodes->at(i) = meshNodes->at(i + 1);
|
meshNodes->at(i) = meshNodes->at(i + 1);
|
||||||
@@ -2586,23 +2165,18 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
|
|||||||
/// valid lat/lon
|
/// valid lat/lon
|
||||||
bool NodeDB::hasValidPosition(const meshtastic_NodeInfoLite *n)
|
bool NodeDB::hasValidPosition(const meshtastic_NodeInfoLite *n)
|
||||||
{
|
{
|
||||||
if (!n)
|
return n->has_position && (n->position.latitude_i != 0 || n->position.longitude_i != 0);
|
||||||
return false;
|
|
||||||
if (n->num == getNodeNum()) {
|
|
||||||
return localPosition.latitude_i != 0 || localPosition.longitude_i != 0;
|
|
||||||
}
|
|
||||||
meshtastic_PositionLite pos;
|
|
||||||
return copyNodePosition(n->num, pos) && (pos.latitude_i != 0 || pos.longitude_i != 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If we have a node / user and they report is_licensed = true
|
/// If we have a node / user and they report is_licensed = true
|
||||||
/// we consider them licensed
|
/// we consider them licensed
|
||||||
UserLicenseStatus NodeDB::getLicenseStatus(uint32_t nodeNum)
|
UserLicenseStatus NodeDB::getLicenseStatus(uint32_t nodeNum)
|
||||||
{
|
{
|
||||||
const meshtastic_NodeInfoLite *info = getMeshNode(nodeNum);
|
meshtastic_NodeInfoLite *info = getMeshNode(nodeNum);
|
||||||
if (!nodeInfoLiteHasUser(info))
|
if (!info || !info->has_user) {
|
||||||
return UserLicenseStatus::NotKnown;
|
return UserLicenseStatus::NotKnown;
|
||||||
return nodeInfoLiteIsLicensed(info) ? UserLicenseStatus::Licensed : UserLicenseStatus::NotLicensed;
|
}
|
||||||
|
return info->user.is_licensed ? UserLicenseStatus::Licensed : UserLicenseStatus::NotLicensed;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !defined(MESHTASTIC_EXCLUDE_PKI)
|
#if !defined(MESHTASTIC_EXCLUDE_PKI)
|
||||||
|
|||||||
+4
-134
@@ -6,12 +6,10 @@
|
|||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
#include <pb_encode.h>
|
#include <pb_encode.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "MeshTypes.h"
|
#include "MeshTypes.h"
|
||||||
#include "NodeStatus.h"
|
#include "NodeStatus.h"
|
||||||
#include "concurrency/Lock.h"
|
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#include "mesh-pb-constants.h"
|
#include "mesh-pb-constants.h"
|
||||||
#include "mesh/generated/meshtastic/mesh.pb.h" // For CriticalErrorCode
|
#include "mesh/generated/meshtastic/mesh.pb.h" // For CriticalErrorCode
|
||||||
@@ -84,9 +82,7 @@ DeviceState versions used to be defined in the .proto file but really only this
|
|||||||
#define SEGMENT_CHANNELS 8
|
#define SEGMENT_CHANNELS 8
|
||||||
#define SEGMENT_NODEDATABASE 16
|
#define SEGMENT_NODEDATABASE 16
|
||||||
|
|
||||||
#define DEVICESTATE_CUR_VER 25
|
#define DEVICESTATE_CUR_VER 24
|
||||||
// Lowest on-disk version we still know how to load. v24 saves are migrated
|
|
||||||
// at boot via the parallel deviceonly_legacy descriptor and re-saved as v25.
|
|
||||||
#define DEVICESTATE_MIN_VER 24
|
#define DEVICESTATE_MIN_VER 24
|
||||||
|
|
||||||
extern meshtastic_DeviceState devicestate;
|
extern meshtastic_DeviceState devicestate;
|
||||||
@@ -170,21 +166,6 @@ class NodeDB
|
|||||||
Observable<const meshtastic::NodeStatus *> newStatus;
|
Observable<const meshtastic::NodeStatus *> newStatus;
|
||||||
pb_size_t numMeshNodes;
|
pb_size_t numMeshNodes;
|
||||||
|
|
||||||
// Satellite per-NodeNum maps for data we used to inline into NodeInfoLite,
|
|
||||||
// gated by MESHTASTIC_EXCLUDE_*DB so STM32WL can omit them.
|
|
||||||
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
|
||||||
std::unordered_map<NodeNum, meshtastic_PositionLite> nodePositions;
|
|
||||||
#endif
|
|
||||||
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
|
||||||
std::unordered_map<NodeNum, meshtastic_DeviceMetrics> nodeTelemetry;
|
|
||||||
#endif
|
|
||||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
|
||||||
std::unordered_map<NodeNum, meshtastic_EnvironmentMetrics> nodeEnvironment;
|
|
||||||
#endif
|
|
||||||
#if !MESHTASTIC_EXCLUDE_STATUSDB
|
|
||||||
std::unordered_map<NodeNum, meshtastic_StatusMessage> nodeStatus;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
bool keyIsLowEntropy = false;
|
bool keyIsLowEntropy = false;
|
||||||
bool hasWarned = false;
|
bool hasWarned = false;
|
||||||
|
|
||||||
@@ -296,43 +277,6 @@ class NodeDB
|
|||||||
virtual meshtastic_NodeInfoLite *getMeshNode(NodeNum n);
|
virtual meshtastic_NodeInfoLite *getMeshNode(NodeNum n);
|
||||||
size_t getNumMeshNodes() { return numMeshNodes; }
|
size_t getNumMeshNodes() { return numMeshNodes; }
|
||||||
|
|
||||||
// Thread-safe satellite-map accessors. Return false if absent or the
|
|
||||||
// corresponding DB is compiled out.
|
|
||||||
bool copyNodePosition(NodeNum n, meshtastic_PositionLite &out) const;
|
|
||||||
bool copyNodeTelemetry(NodeNum n, meshtastic_DeviceMetrics &out) const;
|
|
||||||
bool copyNodeEnvironment(NodeNum n, meshtastic_EnvironmentMetrics &out) const;
|
|
||||||
bool copyNodeStatus(NodeNum n, meshtastic_StatusMessage &out) const;
|
|
||||||
std::vector<NodeNum> snapshotPositionNodeNums(NodeNum exclude) const;
|
|
||||||
std::vector<NodeNum> snapshotTelemetryNodeNums(NodeNum exclude) const;
|
|
||||||
std::vector<NodeNum> snapshotEnvironmentNodeNums(NodeNum exclude) const;
|
|
||||||
std::vector<NodeNum> snapshotStatusNodeNums(NodeNum exclude) const;
|
|
||||||
|
|
||||||
void setNodeStatus(NodeNum n, const meshtastic_StatusMessage &status);
|
|
||||||
void touchNodePositionTime(NodeNum n, uint32_t time);
|
|
||||||
|
|
||||||
bool hasNodePosition(NodeNum n) const
|
|
||||||
{
|
|
||||||
meshtastic_PositionLite scratch;
|
|
||||||
return copyNodePosition(n, scratch);
|
|
||||||
}
|
|
||||||
bool hasNodeTelemetry(NodeNum n) const
|
|
||||||
{
|
|
||||||
meshtastic_DeviceMetrics scratch;
|
|
||||||
return copyNodeTelemetry(n, scratch);
|
|
||||||
}
|
|
||||||
bool hasNodeEnvironment(NodeNum n) const
|
|
||||||
{
|
|
||||||
meshtastic_EnvironmentMetrics scratch;
|
|
||||||
return copyNodeEnvironment(n, scratch);
|
|
||||||
}
|
|
||||||
bool hasNodeStatus(NodeNum n) const
|
|
||||||
{
|
|
||||||
meshtastic_StatusMessage scratch;
|
|
||||||
return copyNodeStatus(n, scratch);
|
|
||||||
}
|
|
||||||
|
|
||||||
void eraseNodeSatellites(NodeNum n);
|
|
||||||
|
|
||||||
UserLicenseStatus getLicenseStatus(uint32_t nodeNum);
|
UserLicenseStatus getLicenseStatus(uint32_t nodeNum);
|
||||||
|
|
||||||
size_t getMaxNodesAllocatedSize()
|
size_t getMaxNodesAllocatedSize()
|
||||||
@@ -341,11 +285,7 @@ class NodeDB
|
|||||||
emptyNodeDatabase.version = DEVICESTATE_CUR_VER;
|
emptyNodeDatabase.version = DEVICESTATE_CUR_VER;
|
||||||
size_t nodeDatabaseSize;
|
size_t nodeDatabaseSize;
|
||||||
pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &emptyNodeDatabase);
|
pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &emptyNodeDatabase);
|
||||||
// Always include satellite slots so backups from higher-cap peers
|
return nodeDatabaseSize + (MAX_NUM_NODES * meshtastic_NodeInfoLite_size);
|
||||||
// decode without truncation, even when our build excludes the DBs.
|
|
||||||
return nodeDatabaseSize + (MAX_NUM_NODES * meshtastic_NodeInfoLite_size) +
|
|
||||||
(MAX_NUM_NODES * meshtastic_NodePositionEntry_size) + (MAX_NUM_NODES * meshtastic_NodeTelemetryEntry_size) +
|
|
||||||
(MAX_NUM_NODES * meshtastic_NodeEnvironmentEntry_size) + (MAX_NUM_NODES * meshtastic_NodeStatusEntry_size);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns true if the maximum number of nodes is reached or we are running low on memory
|
// returns true if the maximum number of nodes is reached or we are running low on memory
|
||||||
@@ -389,7 +329,6 @@ class NodeDB
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
mutable concurrency::Lock satelliteMutex;
|
|
||||||
bool duplicateWarned = false;
|
bool duplicateWarned = false;
|
||||||
bool localPositionUpdatedSinceBoot = false;
|
bool localPositionUpdatedSinceBoot = false;
|
||||||
uint32_t lastNodeDbSave = 0; // when we last saved our db to flash
|
uint32_t lastNodeDbSave = 0; // when we last saved our db to flash
|
||||||
@@ -424,11 +363,6 @@ class NodeDB
|
|||||||
bool saveDeviceStateToDisk();
|
bool saveDeviceStateToDisk();
|
||||||
bool saveNodeDatabaseToDisk();
|
bool saveNodeDatabaseToDisk();
|
||||||
void sortMeshDB();
|
void sortMeshDB();
|
||||||
|
|
||||||
// Defined in NodeDBLegacyMigration.cpp. Decodes /prefs/nodes.proto via
|
|
||||||
// the legacy descriptor and copies entries into the v25 layout. Caller
|
|
||||||
// is responsible for save / install-default on the result.
|
|
||||||
bool migrateLegacyNodeDatabase();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
extern NodeDB *nodeDB;
|
extern NodeDB *nodeDB;
|
||||||
@@ -463,74 +397,10 @@ extern meshtastic_CriticalErrorCode error_code;
|
|||||||
* A numeric error address (nonzero if available)
|
* A numeric error address (nonzero if available)
|
||||||
*/
|
*/
|
||||||
extern uint32_t error_address;
|
extern uint32_t error_address;
|
||||||
// Bit assignments for meshtastic_NodeInfoLite.bitfield.
|
|
||||||
#define NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_SHIFT 0
|
#define NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_SHIFT 0
|
||||||
#define NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK (1u << NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_SHIFT)
|
#define NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK (1 << NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_SHIFT)
|
||||||
#define NODEINFO_BITFIELD_IS_MUTED_SHIFT 1
|
#define NODEINFO_BITFIELD_IS_MUTED_SHIFT 1
|
||||||
#define NODEINFO_BITFIELD_IS_MUTED_MASK (1u << NODEINFO_BITFIELD_IS_MUTED_SHIFT)
|
#define NODEINFO_BITFIELD_IS_MUTED_MASK (1 << NODEINFO_BITFIELD_IS_MUTED_SHIFT)
|
||||||
#define NODEINFO_BITFIELD_VIA_MQTT_SHIFT 2
|
|
||||||
#define NODEINFO_BITFIELD_VIA_MQTT_MASK (1u << NODEINFO_BITFIELD_VIA_MQTT_SHIFT)
|
|
||||||
#define NODEINFO_BITFIELD_IS_FAVORITE_SHIFT 3
|
|
||||||
#define NODEINFO_BITFIELD_IS_FAVORITE_MASK (1u << NODEINFO_BITFIELD_IS_FAVORITE_SHIFT)
|
|
||||||
#define NODEINFO_BITFIELD_IS_IGNORED_SHIFT 4
|
|
||||||
#define NODEINFO_BITFIELD_IS_IGNORED_MASK (1u << NODEINFO_BITFIELD_IS_IGNORED_SHIFT)
|
|
||||||
#define NODEINFO_BITFIELD_HAS_USER_SHIFT 5
|
|
||||||
#define NODEINFO_BITFIELD_HAS_USER_MASK (1u << NODEINFO_BITFIELD_HAS_USER_SHIFT)
|
|
||||||
#define NODEINFO_BITFIELD_IS_LICENSED_SHIFT 6
|
|
||||||
#define NODEINFO_BITFIELD_IS_LICENSED_MASK (1u << NODEINFO_BITFIELD_IS_LICENSED_SHIFT)
|
|
||||||
#define NODEINFO_BITFIELD_IS_UNMESSAGABLE_SHIFT 7
|
|
||||||
#define NODEINFO_BITFIELD_IS_UNMESSAGABLE_MASK (1u << NODEINFO_BITFIELD_IS_UNMESSAGABLE_SHIFT)
|
|
||||||
#define NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_SHIFT 8
|
|
||||||
#define NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK (1u << NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_SHIFT)
|
|
||||||
// Bits 9..31 reserved for future single-bit flags.
|
|
||||||
|
|
||||||
// Convenience accessors so call sites read like the old struct fields.
|
|
||||||
inline bool nodeInfoLiteHasUser(const meshtastic_NodeInfoLite *n)
|
|
||||||
{
|
|
||||||
return n && (n->bitfield & NODEINFO_BITFIELD_HAS_USER_MASK);
|
|
||||||
}
|
|
||||||
inline bool nodeInfoLiteViaMqtt(const meshtastic_NodeInfoLite *n)
|
|
||||||
{
|
|
||||||
return n && (n->bitfield & NODEINFO_BITFIELD_VIA_MQTT_MASK);
|
|
||||||
}
|
|
||||||
inline bool nodeInfoLiteIsFavorite(const meshtastic_NodeInfoLite *n)
|
|
||||||
{
|
|
||||||
return n && (n->bitfield & NODEINFO_BITFIELD_IS_FAVORITE_MASK);
|
|
||||||
}
|
|
||||||
inline bool nodeInfoLiteIsIgnored(const meshtastic_NodeInfoLite *n)
|
|
||||||
{
|
|
||||||
return n && (n->bitfield & NODEINFO_BITFIELD_IS_IGNORED_MASK);
|
|
||||||
}
|
|
||||||
inline bool nodeInfoLiteIsLicensed(const meshtastic_NodeInfoLite *n)
|
|
||||||
{
|
|
||||||
return n && (n->bitfield & NODEINFO_BITFIELD_IS_LICENSED_MASK);
|
|
||||||
}
|
|
||||||
inline bool nodeInfoLiteHasIsUnmessagable(const meshtastic_NodeInfoLite *n)
|
|
||||||
{
|
|
||||||
return n && (n->bitfield & NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK);
|
|
||||||
}
|
|
||||||
inline bool nodeInfoLiteIsUnmessagable(const meshtastic_NodeInfoLite *n)
|
|
||||||
{
|
|
||||||
return n && (n->bitfield & NODEINFO_BITFIELD_IS_UNMESSAGABLE_MASK);
|
|
||||||
}
|
|
||||||
inline bool nodeInfoLiteIsMuted(const meshtastic_NodeInfoLite *n)
|
|
||||||
{
|
|
||||||
return n && (n->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK);
|
|
||||||
}
|
|
||||||
inline bool nodeInfoLiteIsKeyManuallyVerified(const meshtastic_NodeInfoLite *n)
|
|
||||||
{
|
|
||||||
return n && (n->bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void nodeInfoLiteSetBit(meshtastic_NodeInfoLite *n, uint32_t mask, bool value)
|
|
||||||
{
|
|
||||||
if (!n)
|
|
||||||
return;
|
|
||||||
if (value)
|
|
||||||
n->bitfield |= mask;
|
|
||||||
else
|
|
||||||
n->bitfield &= ~mask;
|
|
||||||
}
|
|
||||||
|
|
||||||
#define Module_Config_size \
|
#define Module_Config_size \
|
||||||
(ModuleConfig_CannedMessageConfig_size + ModuleConfig_ExternalNotificationConfig_size + ModuleConfig_MQTTConfig_size + \
|
(ModuleConfig_CannedMessageConfig_size + ModuleConfig_ExternalNotificationConfig_size + ModuleConfig_MQTTConfig_size + \
|
||||||
|
|||||||
@@ -1,129 +0,0 @@
|
|||||||
// Migration from the legacy (pre-v25) NodeDatabase shape to the slim header +
|
|
||||||
// satellite maps layout. Lives in its own translation unit so the bulk
|
|
||||||
// fixed-shape decode + field-by-field copy doesn't clutter NodeDB.cpp.
|
|
||||||
//
|
|
||||||
// Caller (NodeDB::loadFromDisk) decides what to do with the result:
|
|
||||||
// - true -> persist via saveNodeDatabaseToDisk()
|
|
||||||
// - false -> reset via installDefaultNodeDatabase()
|
|
||||||
//
|
|
||||||
// This file (and the deviceonly_legacy proto) can be removed once
|
|
||||||
// DEVICESTATE_MIN_VER advances past 24.
|
|
||||||
|
|
||||||
#include "NodeDB.h"
|
|
||||||
#include "concurrency/LockGuard.h"
|
|
||||||
#include "configuration.h"
|
|
||||||
#include "mesh-pb-constants.h"
|
|
||||||
#include "mesh/generated/meshtastic/deviceonly_legacy.pb.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cstring>
|
|
||||||
#include <pb_decode.h>
|
|
||||||
#include <pb_encode.h>
|
|
||||||
|
|
||||||
bool meshtastic_NodeDatabase_Legacy_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_t *field)
|
|
||||||
{
|
|
||||||
const auto *iter = reinterpret_cast<const pb_field_iter_t *>(field);
|
|
||||||
if (ostream) {
|
|
||||||
const auto *vec = static_cast<const std::vector<meshtastic_NodeInfoLite_Legacy> *>(iter->pData);
|
|
||||||
for (auto item : *vec) {
|
|
||||||
if (!pb_encode_tag_for_field(ostream, iter))
|
|
||||||
return false;
|
|
||||||
if (!pb_encode_submessage(ostream, meshtastic_NodeInfoLite_Legacy_fields, &item))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (istream) {
|
|
||||||
meshtastic_NodeInfoLite_Legacy node;
|
|
||||||
auto *vec = static_cast<std::vector<meshtastic_NodeInfoLite_Legacy> *>(iter->pData);
|
|
||||||
if (istream->bytes_left && pb_decode(istream, meshtastic_NodeInfoLite_Legacy_fields, &node))
|
|
||||||
vec->push_back(node);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool NodeDB::migrateLegacyNodeDatabase()
|
|
||||||
{
|
|
||||||
LOG_WARN("NodeDatabase v%u: migrating to v%u", nodeDatabase.version, DEVICESTATE_CUR_VER);
|
|
||||||
|
|
||||||
// _init_zero brace-inits the embedded std::vector via its explicit
|
|
||||||
// (size_type, allocator) ctor, so default-construct instead.
|
|
||||||
meshtastic_NodeDatabase_Legacy legacyDb{};
|
|
||||||
legacyDb.version = 0;
|
|
||||||
meshtastic_NodeDatabase_Legacy legacyEmpty{};
|
|
||||||
legacyEmpty.version = DEVICESTATE_CUR_VER;
|
|
||||||
size_t legacyEmptyEncoded = 0;
|
|
||||||
pb_get_encoded_size(&legacyEmptyEncoded, meshtastic_NodeDatabase_Legacy_fields, &legacyEmpty);
|
|
||||||
const size_t legacyBufSize = legacyEmptyEncoded + (MAX_NUM_NODES * meshtastic_NodeInfoLite_Legacy_size);
|
|
||||||
auto legacyState = loadProto(nodeDatabaseFileName, legacyBufSize, sizeof(meshtastic_NodeDatabase_Legacy),
|
|
||||||
&meshtastic_NodeDatabase_Legacy_msg, &legacyDb);
|
|
||||||
if (legacyState != LoadFileResult::LOAD_SUCCESS) {
|
|
||||||
LOG_ERROR("Failed to load NodeDatabase via legacy descriptor; installing default");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeDatabase.nodes.clear();
|
|
||||||
const size_t maxToMigrate = std::min<size_t>(legacyDb.nodes.size(), MAX_NUM_NODES);
|
|
||||||
nodeDatabase.nodes.reserve(maxToMigrate);
|
|
||||||
size_t posCount = 0, telCount = 0;
|
|
||||||
{
|
|
||||||
concurrency::LockGuard guard(&satelliteMutex);
|
|
||||||
for (size_t i = 0; i < maxToMigrate; ++i) {
|
|
||||||
const auto &legacy = legacyDb.nodes[i];
|
|
||||||
meshtastic_NodeInfoLite slim = meshtastic_NodeInfoLite_init_default;
|
|
||||||
slim.num = legacy.num;
|
|
||||||
slim.snr = legacy.snr;
|
|
||||||
slim.last_heard = legacy.last_heard;
|
|
||||||
slim.channel = legacy.channel;
|
|
||||||
slim.has_hops_away = legacy.has_hops_away;
|
|
||||||
slim.hops_away = legacy.hops_away;
|
|
||||||
slim.next_hop = legacy.next_hop;
|
|
||||||
slim.bitfield = legacy.bitfield;
|
|
||||||
if (legacy.via_mqtt)
|
|
||||||
slim.bitfield |= NODEINFO_BITFIELD_VIA_MQTT_MASK;
|
|
||||||
if (legacy.is_favorite)
|
|
||||||
slim.bitfield |= NODEINFO_BITFIELD_IS_FAVORITE_MASK;
|
|
||||||
if (legacy.is_ignored)
|
|
||||||
slim.bitfield |= NODEINFO_BITFIELD_IS_IGNORED_MASK;
|
|
||||||
if (legacy.has_user) {
|
|
||||||
slim.bitfield |= NODEINFO_BITFIELD_HAS_USER_MASK;
|
|
||||||
strncpy(slim.long_name, legacy.user.long_name, sizeof(slim.long_name));
|
|
||||||
slim.long_name[sizeof(slim.long_name) - 1] = '\0';
|
|
||||||
strncpy(slim.short_name, legacy.user.short_name, sizeof(slim.short_name));
|
|
||||||
slim.short_name[sizeof(slim.short_name) - 1] = '\0';
|
|
||||||
slim.hw_model = legacy.user.hw_model;
|
|
||||||
slim.role = legacy.user.role;
|
|
||||||
if (legacy.user.is_licensed)
|
|
||||||
slim.bitfield |= NODEINFO_BITFIELD_IS_LICENSED_MASK;
|
|
||||||
slim.public_key.size = legacy.user.public_key.size;
|
|
||||||
memcpy(slim.public_key.bytes, legacy.user.public_key.bytes, sizeof(slim.public_key.bytes));
|
|
||||||
if (legacy.user.has_is_unmessagable) {
|
|
||||||
slim.bitfield |= NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK;
|
|
||||||
if (legacy.user.is_unmessagable)
|
|
||||||
slim.bitfield |= NODEINFO_BITFIELD_IS_UNMESSAGABLE_MASK;
|
|
||||||
}
|
|
||||||
// macaddr deprecated since 1.2.11 - dropped from slim header.
|
|
||||||
}
|
|
||||||
nodeDatabase.nodes.push_back(slim);
|
|
||||||
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
|
||||||
if (legacy.has_position)
|
|
||||||
nodePositions[legacy.num] = legacy.position;
|
|
||||||
#endif
|
|
||||||
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
|
||||||
if (legacy.has_device_metrics)
|
|
||||||
nodeTelemetry[legacy.num] = legacy.device_metrics;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
|
||||||
posCount = nodePositions.size();
|
|
||||||
#endif
|
|
||||||
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
|
||||||
telCount = nodeTelemetry.size();
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
nodeDatabase.version = DEVICESTATE_CUR_VER;
|
|
||||||
meshNodes = &nodeDatabase.nodes;
|
|
||||||
numMeshNodes = nodeDatabase.nodes.size();
|
|
||||||
LOG_INFO("Migrated %u nodes from legacy -> v%u (positions: %u, telemetry: %u)", (unsigned)numMeshNodes, DEVICESTATE_CUR_VER,
|
|
||||||
(unsigned)posCount, (unsigned)telCount);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
+41
-26
@@ -17,7 +17,7 @@
|
|||||||
#define VERBOSE_PACKET_HISTORY 0 // Set to 1 for verbose logging, 2 for heavy debugging
|
#define VERBOSE_PACKET_HISTORY 0 // Set to 1 for verbose logging, 2 for heavy debugging
|
||||||
#define PACKET_HISTORY_TRACE_AGING 1 // Set to 1 to enable logging of the age of re/used history slots
|
#define PACKET_HISTORY_TRACE_AGING 1 // Set to 1 to enable logging of the age of re/used history slots
|
||||||
|
|
||||||
PacketHistory::PacketHistory(uint32_t size) : recentPacketsCapacity(0) // Initialize members
|
PacketHistory::PacketHistory(uint32_t size) : recentPacketsCapacity(0), recentPackets(NULL) // Initialize members
|
||||||
{
|
{
|
||||||
if (size < 4 || size > PACKETHISTORY_MAX) { // Copilot suggested - makes sense
|
if (size < 4 || size > PACKETHISTORY_MAX) { // Copilot suggested - makes sense
|
||||||
LOG_WARN("Packet History - Invalid size %d, using default %d", size, PACKETHISTORY_MAX);
|
LOG_WARN("Packet History - Invalid size %d, using default %d", size, PACKETHISTORY_MAX);
|
||||||
@@ -34,7 +34,7 @@ PacketHistory::PacketHistory(uint32_t size) : recentPacketsCapacity(0) // Initia
|
|||||||
|
|
||||||
// Allocate memory for the recent packets array
|
// Allocate memory for the recent packets array
|
||||||
recentPacketsCapacity = size;
|
recentPacketsCapacity = size;
|
||||||
recentPackets.reset(new PacketRecord[recentPacketsCapacity]);
|
recentPackets = new PacketRecord[recentPacketsCapacity];
|
||||||
if (!recentPackets) { // No logging here, console/log probably uninitialized yet.
|
if (!recentPackets) { // No logging here, console/log probably uninitialized yet.
|
||||||
LOG_ERROR("Packet History - Memory allocation failed for size=%d entries / %d Bytes", size,
|
LOG_ERROR("Packet History - Memory allocation failed for size=%d entries / %d Bytes", size,
|
||||||
sizeof(PacketRecord) * recentPacketsCapacity);
|
sizeof(PacketRecord) * recentPacketsCapacity);
|
||||||
@@ -43,20 +43,33 @@ PacketHistory::PacketHistory(uint32_t size) : recentPacketsCapacity(0) // Initia
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the recent packets array to zero
|
// Initialize the recent packets array to zero
|
||||||
memset(recentPackets.get(), 0, sizeof(PacketRecord) * recentPacketsCapacity);
|
memset(recentPackets, 0, sizeof(PacketRecord) * recentPacketsCapacity);
|
||||||
|
|
||||||
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
|
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
|
||||||
// Allocate hash index with load factor <= 0.5 for short probe chains
|
// Allocate hash index with load factor <= 0.5 for short probe chains
|
||||||
hashCapacity = nextPowerOf2(recentPacketsCapacity * 2);
|
hashCapacity = nextPowerOf2(recentPacketsCapacity * 2);
|
||||||
hashMask = hashCapacity - 1;
|
hashMask = hashCapacity - 1;
|
||||||
hashIndex.reset(new uint16_t[hashCapacity]);
|
hashIndex = new uint16_t[hashCapacity];
|
||||||
if (!hashIndex) {
|
if (!hashIndex) {
|
||||||
LOG_ERROR("Packet History - Hash index allocation failed for %d entries", hashCapacity);
|
LOG_ERROR("Packet History - Hash index allocation failed for %d entries", hashCapacity);
|
||||||
hashCapacity = 0;
|
hashCapacity = 0;
|
||||||
hashMask = 0;
|
hashMask = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
memset(hashIndex.get(), 0xFF, sizeof(uint16_t) * hashCapacity); // Fill with HASH_EMPTY (0xFFFF)
|
memset(hashIndex, 0xFF, sizeof(uint16_t) * hashCapacity); // Fill with HASH_EMPTY (0xFFFF)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
PacketHistory::~PacketHistory()
|
||||||
|
{
|
||||||
|
recentPacketsCapacity = 0;
|
||||||
|
delete[] recentPackets;
|
||||||
|
recentPackets = NULL;
|
||||||
|
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
|
||||||
|
delete[] hashIndex;
|
||||||
|
hashIndex = NULL;
|
||||||
|
hashCapacity = 0;
|
||||||
|
hashMask = 0;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +285,7 @@ void PacketHistory::hashRebuild()
|
|||||||
{
|
{
|
||||||
if (!hashIndex)
|
if (!hashIndex)
|
||||||
return;
|
return;
|
||||||
memset(hashIndex.get(), 0xFF, sizeof(uint16_t) * hashCapacity);
|
memset(hashIndex, 0xFF, sizeof(uint16_t) * hashCapacity);
|
||||||
for (uint32_t i = 0; i < recentPacketsCapacity; i++) {
|
for (uint32_t i = 0; i < recentPacketsCapacity; i++) {
|
||||||
if (recentPackets[i].rxTimeMsec != 0)
|
if (recentPackets[i].rxTimeMsec != 0)
|
||||||
hashInsert(recentPackets[i].sender, recentPackets[i].id, (uint16_t)i);
|
hashInsert(recentPackets[i].sender, recentPackets[i].id, (uint16_t)i);
|
||||||
@@ -319,8 +332,7 @@ PacketHistory::PacketRecord *PacketHistory::find(NodeNum sender, PacketId id)
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Linear scan (sole path when hash excluded, fallback when hash allocation failed)
|
// Linear scan (sole path when hash excluded, fallback when hash allocation failed)
|
||||||
PacketRecord *base = recentPackets.get();
|
for (PacketRecord *it = recentPackets; it < (recentPackets + recentPacketsCapacity); ++it) {
|
||||||
for (PacketRecord *it = base; it < (base + recentPacketsCapacity); ++it) {
|
|
||||||
if (it->id == id && it->sender == sender) {
|
if (it->id == id && it->sender == sender) {
|
||||||
return it;
|
return it;
|
||||||
}
|
}
|
||||||
@@ -334,38 +346,39 @@ void PacketHistory::insert(const PacketRecord &r)
|
|||||||
{
|
{
|
||||||
uint32_t now_millis = millis(); // Should not jump with time changes
|
uint32_t now_millis = millis(); // Should not jump with time changes
|
||||||
uint32_t OldtrxTimeMsec = 0;
|
uint32_t OldtrxTimeMsec = 0;
|
||||||
PacketRecord *base = recentPackets.get();
|
|
||||||
PacketRecord *tu = NULL; // Will insert here.
|
PacketRecord *tu = NULL; // Will insert here.
|
||||||
PacketRecord *it = NULL;
|
PacketRecord *it = NULL;
|
||||||
|
|
||||||
// Find a free, matching or oldest used slot in the recentPackets array
|
// Find a free, matching or oldest used slot in the recentPackets array
|
||||||
for (it = base; it < (base + recentPacketsCapacity); ++it) {
|
for (it = recentPackets; it < (recentPackets + recentPacketsCapacity); ++it) {
|
||||||
if (it->id == 0 && it->sender == 0 /*&& rxTimeMsec == 0*/) { // Record is empty
|
if (it->id == 0 && it->sender == 0 /*&& rxTimeMsec == 0*/) { // Record is empty
|
||||||
tu = it; // Remember the free slot
|
tu = it; // Remember the free slot
|
||||||
#if VERBOSE_PACKET_HISTORY >= 2
|
#if VERBOSE_PACKET_HISTORY >= 2
|
||||||
LOG_DEBUG("Packet History - insert: Free slot@ %d/%d", tu - base, recentPacketsCapacity);
|
LOG_DEBUG("Packet History - insert: Free slot@ %d/%d", tu - recentPackets, recentPacketsCapacity);
|
||||||
#endif
|
#endif
|
||||||
// We have that, Exit the loop
|
// We have that, Exit the loop
|
||||||
it = (base + recentPacketsCapacity);
|
it = (recentPackets + recentPacketsCapacity);
|
||||||
} else if (it->id == r.id && it->sender == r.sender) { // Record matches the packet we want to insert
|
} else if (it->id == r.id && it->sender == r.sender) { // Record matches the packet we want to insert
|
||||||
tu = it; // Remember the matching slot
|
tu = it; // Remember the matching slot
|
||||||
OldtrxTimeMsec = now_millis - it->rxTimeMsec; // ..and save current entry's age
|
OldtrxTimeMsec = now_millis - it->rxTimeMsec; // ..and save current entry's age
|
||||||
#if VERBOSE_PACKET_HISTORY >= 2
|
#if VERBOSE_PACKET_HISTORY >= 2
|
||||||
LOG_DEBUG("Packet History - insert: Matched slot@ %d/%d age=%d", tu - base, recentPacketsCapacity, OldtrxTimeMsec);
|
LOG_DEBUG("Packet History - insert: Matched slot@ %d/%d age=%d", tu - recentPackets, recentPacketsCapacity,
|
||||||
|
OldtrxTimeMsec);
|
||||||
#endif
|
#endif
|
||||||
// We have that, Exit the loop
|
// We have that, Exit the loop
|
||||||
it = (base + recentPacketsCapacity);
|
it = (recentPackets + recentPacketsCapacity);
|
||||||
} else {
|
} else {
|
||||||
if (it->rxTimeMsec == 0) {
|
if (it->rxTimeMsec == 0) {
|
||||||
LOG_WARN(
|
LOG_WARN(
|
||||||
"Packet History - insert: Found packet s=%08x id=%08x with rxTimeMsec = 0, slot %d/%d. Should never happen!",
|
"Packet History - insert: Found packet s=%08x id=%08x with rxTimeMsec = 0, slot %d/%d. Should never happen!",
|
||||||
it->sender, it->id, it - base, recentPacketsCapacity);
|
it->sender, it->id, it - recentPackets, recentPacketsCapacity);
|
||||||
}
|
}
|
||||||
if ((now_millis - it->rxTimeMsec) > OldtrxTimeMsec) { // 49.7 days rollover friendly
|
if ((now_millis - it->rxTimeMsec) > OldtrxTimeMsec) { // 49.7 days rollover friendly
|
||||||
OldtrxTimeMsec = now_millis - it->rxTimeMsec;
|
OldtrxTimeMsec = now_millis - it->rxTimeMsec;
|
||||||
tu = it; // remember the oldest packet
|
tu = it; // remember the oldest packet
|
||||||
#if VERBOSE_PACKET_HISTORY >= 2
|
#if VERBOSE_PACKET_HISTORY >= 2
|
||||||
LOG_DEBUG("Packet History - insert: Older slot@ %d/%d age=%d", tu - base, recentPacketsCapacity, OldtrxTimeMsec);
|
LOG_DEBUG("Packet History - insert: Older slot@ %d/%d age=%d", tu - recentPackets, recentPacketsCapacity,
|
||||||
|
OldtrxTimeMsec);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
// keep looking for oldest till entire array is checked
|
// keep looking for oldest till entire array is checked
|
||||||
@@ -380,11 +393,13 @@ void PacketHistory::insert(const PacketRecord &r)
|
|||||||
|
|
||||||
#if VERBOSE_PACKET_HISTORY
|
#if VERBOSE_PACKET_HISTORY
|
||||||
if (tu->id == 0 && tu->sender == 0) {
|
if (tu->id == 0 && tu->sender == 0) {
|
||||||
LOG_DEBUG("Packet History - insert: slot@ %d/%d is NEW", tu - base, recentPacketsCapacity);
|
LOG_DEBUG("Packet History - insert: slot@ %d/%d is NEW", tu - recentPackets, recentPacketsCapacity);
|
||||||
} else if (tu->id == r.id && tu->sender == r.sender) {
|
} else if (tu->id == r.id && tu->sender == r.sender) {
|
||||||
LOG_DEBUG("Packet History - insert: slot@ %d/%d MATCHED, age=%d", tu - base, recentPacketsCapacity, OldtrxTimeMsec);
|
LOG_DEBUG("Packet History - insert: slot@ %d/%d MATCHED, age=%d", tu - recentPackets, recentPacketsCapacity,
|
||||||
|
OldtrxTimeMsec);
|
||||||
} else {
|
} else {
|
||||||
LOG_DEBUG("Packet History - insert: slot@ %d/%d REUSE OLDEST, age=%d", tu - base, recentPacketsCapacity, OldtrxTimeMsec);
|
LOG_DEBUG("Packet History - insert: slot@ %d/%d REUSE OLDEST, age=%d", tu - recentPackets, recentPacketsCapacity,
|
||||||
|
OldtrxTimeMsec);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -417,9 +432,9 @@ void PacketHistory::insert(const PacketRecord &r)
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if VERBOSE_PACKET_HISTORY
|
#if VERBOSE_PACKET_HISTORY
|
||||||
LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=%08x id=%08x nh=%02x rby=%02x %02x %02x rxT=%d BEFORE", tu - base,
|
LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=%08x id=%08x nh=%02x rby=%02x %02x %02x rxT=%d BEFORE",
|
||||||
recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1], tu->relayed_by[2],
|
tu - recentPackets, recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1],
|
||||||
tu->rxTimeMsec);
|
tu->relayed_by[2], tu->rxTimeMsec);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (r.rxTimeMsec == 0) {
|
if (r.rxTimeMsec == 0) {
|
||||||
@@ -439,16 +454,16 @@ void PacketHistory::insert(const PacketRecord &r)
|
|||||||
*tu = r; // store the packet
|
*tu = r; // store the packet
|
||||||
|
|
||||||
if (!isMatchingSlot) {
|
if (!isMatchingSlot) {
|
||||||
hashInsert(r.sender, r.id, (uint16_t)(tu - base));
|
hashInsert(r.sender, r.id, (uint16_t)(tu - recentPackets));
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
*tu = r; // store the packet
|
*tu = r; // store the packet
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if VERBOSE_PACKET_HISTORY
|
#if VERBOSE_PACKET_HISTORY
|
||||||
LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=%08x id=%08x nh=%02x rby=%02x %02x %02x rxT=%d AFTER", tu - base,
|
LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=%08x id=%08x nh=%02x rby=%02x %02x %02x rxT=%d AFTER",
|
||||||
recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1], tu->relayed_by[2],
|
tu - recentPackets, recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1],
|
||||||
tu->rxTimeMsec);
|
tu->relayed_by[2], tu->rxTimeMsec);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
// Number of relayers we keep track of. Use 6 to be efficient with memory alignment of PacketRecord to 20 bytes
|
// Number of relayers we keep track of. Use 6 to be efficient with memory alignment of PacketRecord to 20 bytes
|
||||||
#define NUM_RELAYERS 6
|
#define NUM_RELAYERS 6
|
||||||
@@ -27,7 +26,7 @@ class PacketHistory
|
|||||||
|
|
||||||
uint32_t recentPacketsCapacity =
|
uint32_t recentPacketsCapacity =
|
||||||
0; // Can be set in constructor, no need to recompile. Used to allocate memory for mx_recentPackets.
|
0; // Can be set in constructor, no need to recompile. Used to allocate memory for mx_recentPackets.
|
||||||
std::unique_ptr<PacketRecord[]> recentPackets; // Simple and fixed in size. Debloat.
|
PacketRecord *recentPackets = NULL; // Simple and fixed in size. Debloat.
|
||||||
|
|
||||||
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
|
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
|
||||||
// Open-addressing hash table for O(1) lookup in find(), replacing the O(N) linear scan.
|
// Open-addressing hash table for O(1) lookup in find(), replacing the O(N) linear scan.
|
||||||
@@ -35,7 +34,7 @@ class PacketHistory
|
|||||||
// The load factor invariant holds permanently: hashCapacity = 2 * nextPowerOf2(recentPacketsCapacity),
|
// The load factor invariant holds permanently: hashCapacity = 2 * nextPowerOf2(recentPacketsCapacity),
|
||||||
// and at most recentPacketsCapacity entries can ever be live (one per recentPackets[] slot).
|
// and at most recentPacketsCapacity entries can ever be live (one per recentPackets[] slot).
|
||||||
static constexpr uint16_t HASH_EMPTY = 0xFFFF;
|
static constexpr uint16_t HASH_EMPTY = 0xFFFF;
|
||||||
std::unique_ptr<uint16_t[]> hashIndex;
|
uint16_t *hashIndex = NULL;
|
||||||
uint32_t hashCapacity = 0; // Always a power of 2
|
uint32_t hashCapacity = 0; // Always a power of 2
|
||||||
uint32_t hashMask = 0; // hashCapacity - 1, for fast modular indexing
|
uint32_t hashMask = 0; // hashCapacity - 1, for fast modular indexing
|
||||||
|
|
||||||
@@ -65,8 +64,11 @@ class PacketHistory
|
|||||||
uint8_t getOurTxHopLimit(const PacketRecord &r);
|
uint8_t getOurTxHopLimit(const PacketRecord &r);
|
||||||
void setOurTxHopLimit(PacketRecord &r, uint8_t hopLimit);
|
void setOurTxHopLimit(PacketRecord &r, uint8_t hopLimit);
|
||||||
|
|
||||||
|
PacketHistory(const PacketHistory &); // non construction-copyable
|
||||||
|
PacketHistory &operator=(const PacketHistory &); // non copyable
|
||||||
public:
|
public:
|
||||||
explicit PacketHistory(uint32_t size = -1); // Constructor with size parameter, default is PACKETHISTORY_MAX
|
explicit PacketHistory(uint32_t size = -1); // Constructor with size parameter, default is PACKETHISTORY_MAX
|
||||||
|
~PacketHistory();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update recentBroadcasts and return true if we have already seen this packet
|
* Update recentBroadcasts and return true if we have already seen this packet
|
||||||
@@ -98,5 +100,5 @@ class PacketHistory
|
|||||||
void removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender);
|
void removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender);
|
||||||
|
|
||||||
// To check if the PacketHistory was initialized correctly by constructor
|
// To check if the PacketHistory was initialized correctly by constructor
|
||||||
bool initOk(void) { return recentPackets != nullptr && recentPacketsCapacity != 0; }
|
bool initOk(void) { return recentPackets != NULL && recentPacketsCapacity != 0; }
|
||||||
};
|
};
|
||||||
|
|||||||
+13
-426
@@ -62,7 +62,7 @@ void PhoneAPI::handleStartConfig()
|
|||||||
onConfigStart();
|
onConfigStart();
|
||||||
|
|
||||||
// even if we were already connected - restart our state machine
|
// even if we were already connected - restart our state machine
|
||||||
if (config_nonce == SPECIAL_NONCE_ONLY_NODES || config_nonce == SPECIAL_NONCE_GRADIENT_ONLY_NODES) {
|
if (config_nonce == SPECIAL_NONCE_ONLY_NODES) {
|
||||||
// If client only wants node info, jump directly to sending nodes
|
// If client only wants node info, jump directly to sending nodes
|
||||||
state = STATE_SEND_OWN_NODEINFO;
|
state = STATE_SEND_OWN_NODEINFO;
|
||||||
LOG_INFO("Client only wants node info, skipping other config");
|
LOG_INFO("Client only wants node info, skipping other config");
|
||||||
@@ -81,15 +81,6 @@ void PhoneAPI::handleStartConfig()
|
|||||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||||
nodeInfoForPhone = {};
|
nodeInfoForPhone = {};
|
||||||
nodeInfoQueue.clear();
|
nodeInfoQueue.clear();
|
||||||
replayQueue.clear();
|
|
||||||
replayPositionOrder.clear();
|
|
||||||
replayTelemetryOrder.clear();
|
|
||||||
replayEnvironmentOrder.clear();
|
|
||||||
replayStatusOrder.clear();
|
|
||||||
replayPositionIndex = 0;
|
|
||||||
replayTelemetryIndex = 0;
|
|
||||||
replayEnvironmentIndex = 0;
|
|
||||||
replayStatusIndex = 0;
|
|
||||||
}
|
}
|
||||||
resetReadIndex();
|
resetReadIndex();
|
||||||
}
|
}
|
||||||
@@ -129,15 +120,6 @@ void PhoneAPI::close()
|
|||||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||||
nodeInfoForPhone = {};
|
nodeInfoForPhone = {};
|
||||||
nodeInfoQueue.clear();
|
nodeInfoQueue.clear();
|
||||||
replayQueue.clear();
|
|
||||||
replayPositionOrder.clear();
|
|
||||||
replayTelemetryOrder.clear();
|
|
||||||
replayEnvironmentOrder.clear();
|
|
||||||
replayStatusOrder.clear();
|
|
||||||
replayPositionIndex = 0;
|
|
||||||
replayTelemetryIndex = 0;
|
|
||||||
replayEnvironmentIndex = 0;
|
|
||||||
replayStatusIndex = 0;
|
|
||||||
}
|
}
|
||||||
packetForPhone = NULL;
|
packetForPhone = NULL;
|
||||||
filesManifest.clear();
|
filesManifest.clear();
|
||||||
@@ -320,7 +302,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
|||||||
nodeInfoForPhone.num = 0;
|
nodeInfoForPhone.num = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (config_nonce == SPECIAL_NONCE_ONLY_NODES || config_nonce == SPECIAL_NONCE_GRADIENT_ONLY_NODES) {
|
if (config_nonce == SPECIAL_NONCE_ONLY_NODES) {
|
||||||
// If client only wants node info, jump directly to sending nodes
|
// If client only wants node info, jump directly to sending nodes
|
||||||
state = STATE_SEND_OTHER_NODEINFOS;
|
state = STATE_SEND_OTHER_NODEINFOS;
|
||||||
onNowHasData(0);
|
onNowHasData(0);
|
||||||
@@ -540,6 +522,11 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
|||||||
// LOG_INFO("nodeinfo: num=0x%x, lastseen=%u, id=%s, name=%s", nodeInfoForPhone.num, nodeInfoForPhone.last_heard,
|
// LOG_INFO("nodeinfo: num=0x%x, lastseen=%u, id=%s, name=%s", nodeInfoForPhone.num, nodeInfoForPhone.last_heard,
|
||||||
// nodeInfoForPhone.user.id, nodeInfoForPhone.user.long_name);
|
// nodeInfoForPhone.user.id, nodeInfoForPhone.user.long_name);
|
||||||
|
|
||||||
|
// Occasional progress logging. (readIndex==2 will be true for the first non-us node)
|
||||||
|
if (readIndex == 2 || readIndex % 20 == 0) {
|
||||||
|
LOG_DEBUG("nodeinfo: %d/%d", readIndex, nodeDB->getNumMeshNodes());
|
||||||
|
}
|
||||||
|
|
||||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag;
|
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag;
|
||||||
fromRadioScratch.node_info = infoToSend;
|
fromRadioScratch.node_info = infoToSend;
|
||||||
prefetchNodeInfos();
|
prefetchNodeInfos();
|
||||||
@@ -547,124 +534,8 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
|||||||
LOG_DEBUG("Done sending %d of %d nodeinfos millis=%u", readIndex, nodeDB->getNumMeshNodes(), millis());
|
LOG_DEBUG("Done sending %d of %d nodeinfos millis=%u", readIndex, nodeDB->getNumMeshNodes(), millis());
|
||||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||||
nodeInfoQueue.clear();
|
nodeInfoQueue.clear();
|
||||||
// Replay states no-op for legacy clients / excluded DBs.
|
|
||||||
state = STATE_REPLAY_POSITIONS;
|
|
||||||
return getFromRadio(buf);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case STATE_REPLAY_POSITIONS: {
|
|
||||||
if (replayPositionOrder.empty() && replayPositionIndex == 0)
|
|
||||||
beginReplayPositions();
|
|
||||||
prefetchReplayPositions();
|
|
||||||
|
|
||||||
meshtastic_MeshPacket pkt = {};
|
|
||||||
bool havePkt = false;
|
|
||||||
{
|
|
||||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
|
||||||
if (!replayQueue.empty()) {
|
|
||||||
pkt = replayQueue.front();
|
|
||||||
replayQueue.pop_front();
|
|
||||||
havePkt = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (havePkt) {
|
|
||||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag;
|
|
||||||
fromRadioScratch.packet = pkt;
|
|
||||||
} else {
|
|
||||||
LOG_DEBUG("Done replaying positions count=%u millis=%u", (unsigned)replayPositionIndex, millis());
|
|
||||||
state = STATE_REPLAY_TELEMETRY;
|
|
||||||
return getFromRadio(buf);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case STATE_REPLAY_TELEMETRY: {
|
|
||||||
if (replayTelemetryOrder.empty() && replayTelemetryIndex == 0)
|
|
||||||
beginReplayTelemetry();
|
|
||||||
prefetchReplayTelemetry();
|
|
||||||
|
|
||||||
meshtastic_MeshPacket pkt = {};
|
|
||||||
bool havePkt = false;
|
|
||||||
{
|
|
||||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
|
||||||
if (!replayQueue.empty()) {
|
|
||||||
pkt = replayQueue.front();
|
|
||||||
replayQueue.pop_front();
|
|
||||||
havePkt = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (havePkt) {
|
|
||||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag;
|
|
||||||
fromRadioScratch.packet = pkt;
|
|
||||||
} else {
|
|
||||||
LOG_DEBUG("Done replaying telemetry count=%u millis=%u", (unsigned)replayTelemetryIndex, millis());
|
|
||||||
state = STATE_REPLAY_ENVIRONMENT;
|
|
||||||
return getFromRadio(buf);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case STATE_REPLAY_ENVIRONMENT: {
|
|
||||||
if (replayEnvironmentOrder.empty() && replayEnvironmentIndex == 0)
|
|
||||||
beginReplayEnvironment();
|
|
||||||
prefetchReplayEnvironment();
|
|
||||||
|
|
||||||
meshtastic_MeshPacket pkt = {};
|
|
||||||
bool havePkt = false;
|
|
||||||
{
|
|
||||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
|
||||||
if (!replayQueue.empty()) {
|
|
||||||
pkt = replayQueue.front();
|
|
||||||
replayQueue.pop_front();
|
|
||||||
havePkt = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (havePkt) {
|
|
||||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag;
|
|
||||||
fromRadioScratch.packet = pkt;
|
|
||||||
} else {
|
|
||||||
LOG_DEBUG("Done replaying environment count=%u millis=%u", (unsigned)replayEnvironmentIndex, millis());
|
|
||||||
state = STATE_REPLAY_STATUS;
|
|
||||||
return getFromRadio(buf);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case STATE_REPLAY_STATUS: {
|
|
||||||
if (replayStatusOrder.empty() && replayStatusIndex == 0)
|
|
||||||
beginReplayStatus();
|
|
||||||
prefetchReplayStatus();
|
|
||||||
|
|
||||||
meshtastic_MeshPacket pkt = {};
|
|
||||||
bool havePkt = false;
|
|
||||||
{
|
|
||||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
|
||||||
if (!replayQueue.empty()) {
|
|
||||||
pkt = replayQueue.front();
|
|
||||||
replayQueue.pop_front();
|
|
||||||
havePkt = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (havePkt) {
|
|
||||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag;
|
|
||||||
fromRadioScratch.packet = pkt;
|
|
||||||
} else {
|
|
||||||
LOG_DEBUG("Done replaying 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();
|
|
||||||
state = STATE_SEND_FILEMANIFEST;
|
state = STATE_SEND_FILEMANIFEST;
|
||||||
|
// Go ahead and send that ID right now
|
||||||
return getFromRadio(buf);
|
return getFromRadio(buf);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -672,9 +543,9 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
|||||||
|
|
||||||
case STATE_SEND_FILEMANIFEST: {
|
case STATE_SEND_FILEMANIFEST: {
|
||||||
LOG_DEBUG("FromRadio=STATE_SEND_FILEMANIFEST");
|
LOG_DEBUG("FromRadio=STATE_SEND_FILEMANIFEST");
|
||||||
// ONLY_NODES variants skip the manifest.
|
// last element
|
||||||
if (config_state == filesManifest.size() || config_nonce == SPECIAL_NONCE_ONLY_NODES ||
|
if (config_state == filesManifest.size() ||
|
||||||
config_nonce == SPECIAL_NONCE_GRADIENT_ONLY_NODES) {
|
config_nonce == SPECIAL_NONCE_ONLY_NODES) { // also handles an empty filesManifest
|
||||||
config_state = 0;
|
config_state = 0;
|
||||||
filesManifest.clear();
|
filesManifest.clear();
|
||||||
// Skip to complete packet
|
// Skip to complete packet
|
||||||
@@ -786,19 +657,15 @@ void PhoneAPI::releaseQueueStatusPhonePacket()
|
|||||||
void PhoneAPI::prefetchNodeInfos()
|
void PhoneAPI::prefetchNodeInfos()
|
||||||
{
|
{
|
||||||
bool added = false;
|
bool added = false;
|
||||||
bool wasEmpty = false;
|
|
||||||
const bool gradient = clientWantsGradientSync();
|
|
||||||
// Keep the queue topped up so BLE reads stay responsive even if DB fetches take a moment.
|
// Keep the queue topped up so BLE reads stay responsive even if DB fetches take a moment.
|
||||||
{
|
{
|
||||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||||
wasEmpty = nodeInfoQueue.empty();
|
|
||||||
while (nodeInfoQueue.size() < kNodePrefetchDepth) {
|
while (nodeInfoQueue.size() < kNodePrefetchDepth) {
|
||||||
auto nextNode = nodeDB->readNextMeshNode(readIndex);
|
auto nextNode = nodeDB->readNextMeshNode(readIndex);
|
||||||
if (!nextNode)
|
if (!nextNode)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
auto info =
|
auto info = TypeConversions::ConvertToNodeInfo(nextNode);
|
||||||
gradient ? TypeConversions::ConvertToNodeInfoThin(nextNode) : TypeConversions::ConvertToNodeInfo(nextNode);
|
|
||||||
bool isUs = info.num == nodeDB->getNodeNum();
|
bool isUs = info.num == nodeDB->getNodeNum();
|
||||||
info.hops_away = isUs ? 0 : info.hops_away;
|
info.hops_away = isUs ? 0 : info.hops_away;
|
||||||
info.last_heard = isUs ? getValidTime(RTCQualityFromNet) : info.last_heard;
|
info.last_heard = isUs ? getValidTime(RTCQualityFromNet) : info.last_heard;
|
||||||
@@ -806,269 +673,14 @@ void PhoneAPI::prefetchNodeInfos()
|
|||||||
info.via_mqtt = isUs ? false : info.via_mqtt;
|
info.via_mqtt = isUs ? false : info.via_mqtt;
|
||||||
info.is_favorite = info.is_favorite || isUs;
|
info.is_favorite = info.is_favorite || isUs;
|
||||||
nodeInfoQueue.push_back(info);
|
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;
|
added = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (added && wasEmpty)
|
if (added)
|
||||||
onNowHasData(0);
|
onNowHasData(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
meshtastic_MeshPacket PhoneAPI::makeReplayPositionPacket(NodeNum num, const meshtastic_PositionLite &pos)
|
|
||||||
{
|
|
||||||
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default;
|
|
||||||
pkt.from = num;
|
|
||||||
pkt.to = nodeDB->getNodeNum();
|
|
||||||
pkt.id = generatePacketId();
|
|
||||||
pkt.rx_time = pos.time;
|
|
||||||
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 = nodeDB->getNodeNum();
|
|
||||||
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.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 state arm
|
|
||||||
// immediately drains and advances.
|
|
||||||
replayPositionOrder.clear();
|
|
||||||
replayPositionIndex = 0;
|
|
||||||
#else
|
|
||||||
if (!clientWantsGradientSync()) {
|
|
||||||
replayPositionOrder.clear();
|
|
||||||
replayPositionIndex = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 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
|
|
||||||
if (!clientWantsGradientSync())
|
|
||||||
return;
|
|
||||||
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
|
|
||||||
if (!clientWantsGradientSync()) {
|
|
||||||
replayTelemetryOrder.clear();
|
|
||||||
replayTelemetryIndex = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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
|
|
||||||
if (!clientWantsGradientSync())
|
|
||||||
return;
|
|
||||||
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 = nodeDB->getNodeNum();
|
|
||||||
pkt.id = generatePacketId();
|
|
||||||
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
|
||||||
pkt.rx_time = header ? header->last_heard : 0;
|
|
||||||
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
|
|
||||||
if (!clientWantsGradientSync()) {
|
|
||||||
replayEnvironmentOrder.clear();
|
|
||||||
replayEnvironmentIndex = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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
|
|
||||||
if (!clientWantsGradientSync())
|
|
||||||
return;
|
|
||||||
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 = nodeDB->getNodeNum();
|
|
||||||
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.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
|
|
||||||
if (!clientWantsGradientSync()) {
|
|
||||||
replayStatusOrder.clear();
|
|
||||||
replayStatusIndex = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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
|
|
||||||
if (!clientWantsGradientSync())
|
|
||||||
return;
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
void PhoneAPI::releaseMqttClientProxyPhonePacket()
|
void PhoneAPI::releaseMqttClientProxyPhonePacket()
|
||||||
{
|
{
|
||||||
if (mqttClientProxyMessageForPhone) {
|
if (mqttClientProxyMessageForPhone) {
|
||||||
@@ -1115,31 +727,6 @@ bool PhoneAPI::available()
|
|||||||
PREFETCH_NODEINFO:
|
PREFETCH_NODEINFO:
|
||||||
prefetchNodeInfos();
|
prefetchNodeInfos();
|
||||||
return true;
|
return true;
|
||||||
case STATE_REPLAY_POSITIONS: {
|
|
||||||
// Prime the iterator if we haven't yet, then top up the queue.
|
|
||||||
if (replayPositionOrder.empty() && replayPositionIndex == 0)
|
|
||||||
beginReplayPositions();
|
|
||||||
prefetchReplayPositions();
|
|
||||||
return true; // Always advance state machine; arm itself transitions when drained
|
|
||||||
}
|
|
||||||
case STATE_REPLAY_TELEMETRY: {
|
|
||||||
if (replayTelemetryOrder.empty() && replayTelemetryIndex == 0)
|
|
||||||
beginReplayTelemetry();
|
|
||||||
prefetchReplayTelemetry();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case STATE_REPLAY_ENVIRONMENT: {
|
|
||||||
if (replayEnvironmentOrder.empty() && replayEnvironmentIndex == 0)
|
|
||||||
beginReplayEnvironment();
|
|
||||||
prefetchReplayEnvironment();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case STATE_REPLAY_STATUS: {
|
|
||||||
if (replayStatusOrder.empty() && replayStatusIndex == 0)
|
|
||||||
beginReplayStatus();
|
|
||||||
prefetchReplayStatus();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case STATE_SEND_PACKETS: {
|
case STATE_SEND_PACKETS: {
|
||||||
if (!queueStatusPacketForPhone)
|
if (!queueStatusPacketForPhone)
|
||||||
queueStatusPacketForPhone = service->getQueueStatusForPhone();
|
queueStatusPacketForPhone = service->getQueueStatusForPhone();
|
||||||
|
|||||||
+1
-44
@@ -4,16 +4,12 @@
|
|||||||
#include "concurrency/Lock.h"
|
#include "concurrency/Lock.h"
|
||||||
#include "mesh-pb-constants.h"
|
#include "mesh-pb-constants.h"
|
||||||
#include "meshtastic/portnums.pb.h"
|
#include "meshtastic/portnums.pb.h"
|
||||||
#include <cstdint>
|
|
||||||
#include <deque>
|
#include <deque>
|
||||||
#include <iterator>
|
#include <iterator>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
// NodeNum stored as raw uint32_t below; including MeshTypes.h here breaks the
|
|
||||||
// LOG_WARN include order via Arduino.h/MemoryPool.h.
|
|
||||||
|
|
||||||
// Make sure that we never let our packets grow too large for one BLE packet
|
// Make sure that we never let our packets grow too large for one BLE packet
|
||||||
#define MAX_TO_FROM_RADIO_SIZE 512
|
#define MAX_TO_FROM_RADIO_SIZE 512
|
||||||
|
|
||||||
@@ -26,9 +22,6 @@
|
|||||||
|
|
||||||
#define SPECIAL_NONCE_ONLY_CONFIG 69420
|
#define SPECIAL_NONCE_ONLY_CONFIG 69420
|
||||||
#define SPECIAL_NONCE_ONLY_NODES 69421 // ( ͡° ͜ʖ ͡°)
|
#define SPECIAL_NONCE_ONLY_NODES 69421 // ( ͡° ͜ʖ ͡°)
|
||||||
// Gradient sync: phone sends one of these to opt into thin-header + replay.
|
|
||||||
#define SPECIAL_NONCE_GRADIENT_SYNC 69422
|
|
||||||
#define SPECIAL_NONCE_GRADIENT_ONLY_NODES 69423
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides our protobuf based API which phone/PC clients can use to talk to our device
|
* Provides our protobuf based API which phone/PC clients can use to talk to our device
|
||||||
@@ -52,13 +45,7 @@ class PhoneAPI
|
|||||||
STATE_SEND_CONFIG, // Replacement for the old Radioconfig
|
STATE_SEND_CONFIG, // Replacement for the old Radioconfig
|
||||||
STATE_SEND_MODULECONFIG, // Send Module specific config
|
STATE_SEND_MODULECONFIG, // Send Module specific config
|
||||||
STATE_SEND_OTHER_NODEINFOS, // states progress in this order as the device sends to to the client
|
STATE_SEND_OTHER_NODEINFOS, // states progress in this order as the device sends to to the client
|
||||||
// Drain satellite DBs as synthetic POSITION_APP / TELEMETRY_APP /
|
STATE_SEND_FILEMANIFEST, // Send file manifest
|
||||||
// NODE_STATUS_APP packets when the phone opted into gradient sync.
|
|
||||||
STATE_REPLAY_POSITIONS,
|
|
||||||
STATE_REPLAY_TELEMETRY,
|
|
||||||
STATE_REPLAY_ENVIRONMENT,
|
|
||||||
STATE_REPLAY_STATUS,
|
|
||||||
STATE_SEND_FILEMANIFEST, // Send file manifest
|
|
||||||
STATE_SEND_COMPLETE_ID,
|
STATE_SEND_COMPLETE_ID,
|
||||||
STATE_SEND_PACKETS // send packets or debug strings
|
STATE_SEND_PACKETS // send packets or debug strings
|
||||||
};
|
};
|
||||||
@@ -101,20 +88,6 @@ class PhoneAPI
|
|||||||
// Protect nodeInfoForPhone + nodeInfoQueue because NimBLE callbacks run in a separate FreeRTOS task.
|
// Protect nodeInfoForPhone + nodeInfoQueue because NimBLE callbacks run in a separate FreeRTOS task.
|
||||||
concurrency::Lock nodeInfoMutex;
|
concurrency::Lock nodeInfoMutex;
|
||||||
|
|
||||||
// Synthetic-packet replay queue (paced via prefetch).
|
|
||||||
std::deque<meshtastic_MeshPacket> replayQueue;
|
|
||||||
static constexpr size_t kReplayPrefetchDepth = 4;
|
|
||||||
// NodeNum snapshots taken at each replay phase start so concurrent
|
|
||||||
// satellite-map mutations don't invalidate iteration.
|
|
||||||
std::vector<uint32_t> replayPositionOrder;
|
|
||||||
std::vector<uint32_t> replayTelemetryOrder;
|
|
||||||
std::vector<uint32_t> replayEnvironmentOrder;
|
|
||||||
std::vector<uint32_t> replayStatusOrder;
|
|
||||||
size_t replayPositionIndex = 0;
|
|
||||||
size_t replayTelemetryIndex = 0;
|
|
||||||
size_t replayEnvironmentIndex = 0;
|
|
||||||
size_t replayStatusIndex = 0;
|
|
||||||
|
|
||||||
meshtastic_ToRadio toRadioScratch = {
|
meshtastic_ToRadio toRadioScratch = {
|
||||||
0}; // this is a static scratch object, any data must be copied elsewhere before returning
|
0}; // this is a static scratch object, any data must be copied elsewhere before returning
|
||||||
|
|
||||||
@@ -164,10 +137,6 @@ class PhoneAPI
|
|||||||
|
|
||||||
bool isConnected() { return state != STATE_SEND_NOTHING; }
|
bool isConnected() { return state != STATE_SEND_NOTHING; }
|
||||||
bool isSendingPackets() { return state == STATE_SEND_PACKETS; }
|
bool isSendingPackets() { return state == STATE_SEND_PACKETS; }
|
||||||
bool clientWantsGradientSync() const
|
|
||||||
{
|
|
||||||
return config_nonce == SPECIAL_NONCE_GRADIENT_SYNC || config_nonce == SPECIAL_NONCE_GRADIENT_ONLY_NODES;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
/// Our fromradio packet while it is being assembled
|
/// Our fromradio packet while it is being assembled
|
||||||
@@ -216,18 +185,6 @@ class PhoneAPI
|
|||||||
void releaseQueueStatusPhonePacket();
|
void releaseQueueStatusPhonePacket();
|
||||||
|
|
||||||
void prefetchNodeInfos();
|
void prefetchNodeInfos();
|
||||||
void beginReplayPositions();
|
|
||||||
void prefetchReplayPositions();
|
|
||||||
void beginReplayTelemetry();
|
|
||||||
void prefetchReplayTelemetry();
|
|
||||||
void beginReplayEnvironment();
|
|
||||||
void prefetchReplayEnvironment();
|
|
||||||
void beginReplayStatus();
|
|
||||||
void prefetchReplayStatus();
|
|
||||||
meshtastic_MeshPacket makeReplayPositionPacket(uint32_t num, const meshtastic_PositionLite &pos);
|
|
||||||
meshtastic_MeshPacket makeReplayTelemetryPacket(uint32_t num, const meshtastic_DeviceMetrics &metrics);
|
|
||||||
meshtastic_MeshPacket makeReplayEnvironmentPacket(uint32_t num, const meshtastic_EnvironmentMetrics &env);
|
|
||||||
meshtastic_MeshPacket makeReplayStatusPacket(uint32_t num, const meshtastic_StatusMessage &status);
|
|
||||||
|
|
||||||
void releaseMqttClientProxyPhonePacket();
|
void releaseMqttClientProxyPhonePacket();
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ template <class T> class ProtobufModule : protected SinglePortModule
|
|||||||
const char *getSenderShortName(const meshtastic_MeshPacket &mp)
|
const char *getSenderShortName(const meshtastic_MeshPacket &mp)
|
||||||
{
|
{
|
||||||
auto node = nodeDB->getMeshNode(getFrom(&mp));
|
auto node = nodeDB->getMeshNode(getFrom(&mp));
|
||||||
const char *sender = (node && nodeInfoLiteHasUser(node) && node->short_name[0]) ? node->short_name : "???";
|
const char *sender = (node) ? node->user.short_name : "???";
|
||||||
return sender;
|
return sender;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,4 +121,4 @@ template <class T> class ProtobufModule : protected SinglePortModule
|
|||||||
return alterReceivedProtobuf(mp, decoded);
|
return alterReceivedProtobuf(mp, decoded);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -252,7 +252,7 @@ bool RF95Interface::reconfigure()
|
|||||||
|
|
||||||
startReceive(); // restart receiving
|
startReceive(); // restart receiving
|
||||||
|
|
||||||
return true;
|
return RADIOLIB_ERR_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
#include "LR1110Interface.h"
|
#include "LR1110Interface.h"
|
||||||
#include "LR1120Interface.h"
|
#include "LR1120Interface.h"
|
||||||
#include "LR1121Interface.h"
|
#include "LR1121Interface.h"
|
||||||
#include "LR2021Interface.h"
|
|
||||||
#include "MeshRadio.h"
|
#include "MeshRadio.h"
|
||||||
#include "MeshService.h"
|
#include "MeshService.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
@@ -479,20 +478,6 @@ std::unique_ptr<RadioInterface> initLoRa()
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(USE_LR2021) && RADIOLIB_EXCLUDE_LR2021 != 1
|
|
||||||
if (!rIf) {
|
|
||||||
rIf = std::unique_ptr<LR2021Interface>(
|
|
||||||
new LR2021Interface(loraHal, LR2021_SPI_NSS_PIN, LR2021_IRQ_PIN, LR2021_NRESET_PIN, LR2021_BUSY_PIN));
|
|
||||||
if (!rIf->init()) {
|
|
||||||
LOG_WARN("No LR2021 radio");
|
|
||||||
rIf = nullptr;
|
|
||||||
} else {
|
|
||||||
LOG_INFO("LR2021 init success");
|
|
||||||
radioType = LR2021_RADIO;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(USE_SX1280) && RADIOLIB_EXCLUDE_SX128X != 1
|
#if defined(USE_SX1280) && RADIOLIB_EXCLUDE_SX128X != 1
|
||||||
if (!rIf) {
|
if (!rIf) {
|
||||||
rIf = std::unique_ptr<SX1280Interface>(new SX1280Interface(loraHal, SX128X_CS, SX128X_DIO1, SX128X_RESET, SX128X_BUSY));
|
rIf = std::unique_ptr<SX1280Interface>(new SX1280Interface(loraHal, SX128X_CS, SX128X_DIO1, SX128X_RESET, SX128X_BUSY));
|
||||||
|
|||||||
@@ -179,6 +179,8 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
|
|||||||
/** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */
|
/** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */
|
||||||
virtual bool findInTxQueue(NodeNum from, PacketId id) override;
|
virtual bool findInTxQueue(NodeNum from, PacketId id) override;
|
||||||
|
|
||||||
|
uint8_t packetsInTxQueue() { return txQueue.getMaxLen() - txQueue.getFree(); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request randomness sourced from the LoRa modem, if supported by the active RadioLib interface.
|
* Request randomness sourced from the LoRa modem, if supported by the active RadioLib interface.
|
||||||
* @return true if len bytes were produced, false otherwise.
|
* @return true if len bytes were produced, false otherwise.
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ void ReliableRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtas
|
|||||||
sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0);
|
sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0);
|
||||||
}
|
}
|
||||||
} else if (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag && p->channel == 0 &&
|
} else if (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag && p->channel == 0 &&
|
||||||
(nodeDB->getMeshNode(p->from) == nullptr || nodeDB->getMeshNode(p->from)->public_key.size == 0)) {
|
(nodeDB->getMeshNode(p->from) == nullptr || nodeDB->getMeshNode(p->from)->user.public_key.size == 0)) {
|
||||||
LOG_INFO("PKI packet from unknown node, send PKI_UNKNOWN_PUBKEY");
|
LOG_INFO("PKI packet from unknown node, send PKI_UNKNOWN_PUBKEY");
|
||||||
sendAckNak(meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY, getFrom(p), p->id, channels.getPrimaryIndex(),
|
sendAckNak(meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY, getFrom(p), p->id, channels.getPrimaryIndex(),
|
||||||
routingModule->getHopLimitForResponse(*p));
|
routingModule->getHopLimitForResponse(*p));
|
||||||
|
|||||||
+17
-17
@@ -120,17 +120,17 @@ bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p)
|
|||||||
if (!node)
|
if (!node)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Check 1: is_favorite (cheapest - single bit test)
|
// Check 1: is_favorite (cheapest - single bool)
|
||||||
if (!nodeInfoLiteIsFavorite(node))
|
if (!node->is_favorite)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Check 2: has_user (cheap - single bit test)
|
// Check 2: has_user (cheap - single bool)
|
||||||
if (!nodeInfoLiteHasUser(node))
|
if (!node->has_user)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Check 3: role check (moderate cost - multiple comparisons)
|
// Check 3: role check (moderate cost - multiple comparisons)
|
||||||
if (!IS_ONE_OF(node->role, meshtastic_Config_DeviceConfig_Role_ROUTER, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE,
|
if (!IS_ONE_OF(node->user.role, meshtastic_Config_DeviceConfig_Role_ROUTER,
|
||||||
meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) {
|
meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,7 +329,7 @@ ErrorCode Router::send(meshtastic_MeshPacket *p)
|
|||||||
cn->reply_id = p->id;
|
cn->reply_id = p->id;
|
||||||
cn->level = meshtastic_LogRecord_Level_WARNING;
|
cn->level = meshtastic_LogRecord_Level_WARNING;
|
||||||
cn->time = getValidTime(RTCQualityFromNet);
|
cn->time = getValidTime(RTCQualityFromNet);
|
||||||
snprintf(cn->message, sizeof(cn->message), "Duty cycle limit exceeded. You can send again in %d mins", silentMinutes);
|
sprintf(cn->message, "Duty cycle limit exceeded. You can send again in %d mins", silentMinutes);
|
||||||
service->sendClientNotification(cn);
|
service->sendClientNotification(cn);
|
||||||
|
|
||||||
meshtastic_Routing_Error err = meshtastic_Routing_Error_DUTY_CYCLE_LIMIT;
|
meshtastic_Routing_Error err = meshtastic_Routing_Error_DUTY_CYCLE_LIMIT;
|
||||||
@@ -433,7 +433,7 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
|||||||
concurrency::LockGuard g(cryptLock);
|
concurrency::LockGuard g(cryptLock);
|
||||||
|
|
||||||
if (config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_KNOWN_ONLY &&
|
if (config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_KNOWN_ONLY &&
|
||||||
!nodeInfoLiteHasUser(nodeDB->getMeshNode(p->from))) {
|
(nodeDB->getMeshNode(p->from) == NULL || !nodeDB->getMeshNode(p->from)->has_user)) {
|
||||||
LOG_DEBUG("Node 0x%x not in nodeDB-> Rebroadcast mode KNOWN_ONLY will ignore packet", p->from);
|
LOG_DEBUG("Node 0x%x not in nodeDB-> Rebroadcast mode KNOWN_ONLY will ignore packet", p->from);
|
||||||
return DecodeState::DECODE_FAILURE;
|
return DecodeState::DECODE_FAILURE;
|
||||||
}
|
}
|
||||||
@@ -451,11 +451,11 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
|||||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||||
// Attempt PKI decryption first
|
// Attempt PKI decryption first
|
||||||
if (p->channel == 0 && isToUs(p) && p->to > 0 && !isBroadcast(p->to) && nodeDB->getMeshNode(p->from) != nullptr &&
|
if (p->channel == 0 && isToUs(p) && p->to > 0 && !isBroadcast(p->to) && nodeDB->getMeshNode(p->from) != nullptr &&
|
||||||
nodeDB->getMeshNode(p->from)->public_key.size > 0 && nodeDB->getMeshNode(p->to) != nullptr &&
|
nodeDB->getMeshNode(p->from)->user.public_key.size > 0 && nodeDB->getMeshNode(p->to)->user.public_key.size > 0 &&
|
||||||
nodeDB->getMeshNode(p->to)->public_key.size > 0 && rawSize > MESHTASTIC_PKC_OVERHEAD) {
|
rawSize > MESHTASTIC_PKC_OVERHEAD) {
|
||||||
LOG_DEBUG("Attempt PKI decryption");
|
LOG_DEBUG("Attempt PKI decryption");
|
||||||
|
|
||||||
if (crypto->decryptCurve25519(p->from, nodeDB->getMeshNode(p->from)->public_key, p->id, rawSize, p->encrypted.bytes,
|
if (crypto->decryptCurve25519(p->from, nodeDB->getMeshNode(p->from)->user.public_key, p->id, rawSize, p->encrypted.bytes,
|
||||||
bytes)) {
|
bytes)) {
|
||||||
LOG_INFO("PKI Decryption worked!");
|
LOG_INFO("PKI Decryption worked!");
|
||||||
|
|
||||||
@@ -467,7 +467,7 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
|||||||
decrypted = true;
|
decrypted = true;
|
||||||
LOG_INFO("Packet decrypted using PKI!");
|
LOG_INFO("Packet decrypted using PKI!");
|
||||||
p->pki_encrypted = true;
|
p->pki_encrypted = true;
|
||||||
memcpy(&p->public_key.bytes, nodeDB->getMeshNode(p->from)->public_key.bytes, 32);
|
memcpy(&p->public_key.bytes, nodeDB->getMeshNode(p->from)->user.public_key.bytes, 32);
|
||||||
p->public_key.size = 32;
|
p->public_key.size = 32;
|
||||||
p->decoded = decodedtmp;
|
p->decoded = decodedtmp;
|
||||||
p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded
|
p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded
|
||||||
@@ -665,18 +665,18 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
|
|||||||
if (numbytes + MESHTASTIC_HEADER_LENGTH + MESHTASTIC_PKC_OVERHEAD > MAX_LORA_PAYLOAD_LEN)
|
if (numbytes + MESHTASTIC_HEADER_LENGTH + MESHTASTIC_PKC_OVERHEAD > MAX_LORA_PAYLOAD_LEN)
|
||||||
return meshtastic_Routing_Error_TOO_LARGE;
|
return meshtastic_Routing_Error_TOO_LARGE;
|
||||||
// Check for a known public key for the destination
|
// Check for a known public key for the destination
|
||||||
if (node == nullptr || node->public_key.size != 32) {
|
if (node == nullptr || node->user.public_key.size != 32) {
|
||||||
LOG_WARN("Unknown public key for destination node 0x%08x (portnum %d), refusing to send legacy DM", p->to,
|
LOG_WARN("Unknown public key for destination node 0x%08x (portnum %d), refusing to send legacy DM", p->to,
|
||||||
p->decoded.portnum);
|
p->decoded.portnum);
|
||||||
return meshtastic_Routing_Error_PKI_SEND_FAIL_PUBLIC_KEY;
|
return meshtastic_Routing_Error_PKI_SEND_FAIL_PUBLIC_KEY;
|
||||||
}
|
}
|
||||||
if (p->pki_encrypted && !memfll(p->public_key.bytes, 0, 32) &&
|
if (p->pki_encrypted && !memfll(p->public_key.bytes, 0, 32) &&
|
||||||
memcmp(p->public_key.bytes, node->public_key.bytes, 32) != 0) {
|
memcmp(p->public_key.bytes, node->user.public_key.bytes, 32) != 0) {
|
||||||
LOG_WARN("Client public key differs from requested: 0x%02x, stored key begins 0x%02x", *p->public_key.bytes,
|
LOG_WARN("Client public key differs from requested: 0x%02x, stored key begins 0x%02x", *p->public_key.bytes,
|
||||||
*node->public_key.bytes);
|
*node->user.public_key.bytes);
|
||||||
return meshtastic_Routing_Error_PKI_FAILED;
|
return meshtastic_Routing_Error_PKI_FAILED;
|
||||||
}
|
}
|
||||||
crypto->encryptCurve25519(p->to, getFrom(p), node->public_key, p->id, numbytes, bytes, p->encrypted.bytes);
|
crypto->encryptCurve25519(p->to, getFrom(p), node->user.public_key, p->id, numbytes, bytes, p->encrypted.bytes);
|
||||||
numbytes += MESHTASTIC_PKC_OVERHEAD;
|
numbytes += MESHTASTIC_PKC_OVERHEAD;
|
||||||
p->channel = 0;
|
p->channel = 0;
|
||||||
p->pki_encrypted = true;
|
p->pki_encrypted = true;
|
||||||
@@ -860,7 +860,7 @@ void Router::perhapsHandleReceived(meshtastic_MeshPacket *p)
|
|||||||
}
|
}
|
||||||
|
|
||||||
meshtastic_NodeInfoLite const *node = nodeDB->getMeshNode(p->from);
|
meshtastic_NodeInfoLite const *node = nodeDB->getMeshNode(p->from);
|
||||||
if (nodeInfoLiteIsIgnored(node)) {
|
if (node != NULL && node->is_ignored) {
|
||||||
LOG_DEBUG("Ignore msg, 0x%x is ignored", p->from);
|
LOG_DEBUG("Ignore msg, 0x%x is ignored", p->from);
|
||||||
packetPool.release(p);
|
packetPool.release(p);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ template <typename T> bool SX126xInterface<T>::reconfigure()
|
|||||||
|
|
||||||
startReceive(); // restart receiving
|
startReceive(); // restart receiving
|
||||||
|
|
||||||
return true;
|
return RADIOLIB_ERR_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename T> void SX126xInterface<T>::disableInterrupt()
|
template <typename T> void SX126xInterface<T>::disableInterrupt()
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ template <typename T> bool SX128xInterface<T>::reconfigure()
|
|||||||
|
|
||||||
startReceive(); // restart receiving
|
startReceive(); // restart receiving
|
||||||
|
|
||||||
return true;
|
return RADIOLIB_ERR_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename T> void SX128xInterface<T>::disableInterrupt()
|
template <typename T> void SX128xInterface<T>::disableInterrupt()
|
||||||
|
|||||||
@@ -3,9 +3,7 @@
|
|||||||
#include "mesh/generated/meshtastic/mesh.pb.h"
|
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||||
#include "meshUtils.h"
|
#include "meshUtils.h"
|
||||||
|
|
||||||
meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite,
|
meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite)
|
||||||
const meshtastic_PositionLite *position,
|
|
||||||
const meshtastic_DeviceMetrics *deviceMetrics)
|
|
||||||
{
|
{
|
||||||
meshtastic_NodeInfo info = meshtastic_NodeInfo_init_default;
|
meshtastic_NodeInfo info = meshtastic_NodeInfo_init_default;
|
||||||
|
|
||||||
@@ -13,56 +11,42 @@ meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfo
|
|||||||
info.snr = lite->snr;
|
info.snr = lite->snr;
|
||||||
info.last_heard = lite->last_heard;
|
info.last_heard = lite->last_heard;
|
||||||
info.channel = lite->channel;
|
info.channel = lite->channel;
|
||||||
info.via_mqtt = nodeInfoLiteViaMqtt(lite);
|
info.via_mqtt = lite->via_mqtt;
|
||||||
info.is_favorite = nodeInfoLiteIsFavorite(lite);
|
info.is_favorite = lite->is_favorite;
|
||||||
info.is_ignored = nodeInfoLiteIsIgnored(lite);
|
info.is_ignored = lite->is_ignored;
|
||||||
info.is_key_manually_verified = nodeInfoLiteIsKeyManuallyVerified(lite);
|
info.is_key_manually_verified = lite->bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||||
info.is_muted = nodeInfoLiteIsMuted(lite);
|
info.is_muted = lite->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK;
|
||||||
|
|
||||||
if (lite->has_hops_away) {
|
if (lite->has_hops_away) {
|
||||||
info.has_hops_away = true;
|
info.has_hops_away = true;
|
||||||
info.hops_away = lite->hops_away;
|
info.hops_away = lite->hops_away;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (position) {
|
if (lite->has_position) {
|
||||||
info.has_position = true;
|
info.has_position = true;
|
||||||
if (position->latitude_i != 0)
|
if (lite->position.latitude_i != 0)
|
||||||
info.position.has_latitude_i = true;
|
info.position.has_latitude_i = true;
|
||||||
info.position.latitude_i = position->latitude_i;
|
info.position.latitude_i = lite->position.latitude_i;
|
||||||
if (position->longitude_i != 0)
|
if (lite->position.longitude_i != 0)
|
||||||
info.position.has_longitude_i = true;
|
info.position.has_longitude_i = true;
|
||||||
info.position.longitude_i = position->longitude_i;
|
info.position.longitude_i = lite->position.longitude_i;
|
||||||
if (position->altitude != 0)
|
if (lite->position.altitude != 0)
|
||||||
info.position.has_altitude = true;
|
info.position.has_altitude = true;
|
||||||
info.position.altitude = position->altitude;
|
info.position.altitude = lite->position.altitude;
|
||||||
info.position.location_source = position->location_source;
|
info.position.location_source = lite->position.location_source;
|
||||||
info.position.time = position->time;
|
info.position.time = lite->position.time;
|
||||||
}
|
}
|
||||||
if (nodeInfoLiteHasUser(lite)) {
|
if (lite->has_user) {
|
||||||
info.has_user = true;
|
info.has_user = true;
|
||||||
info.user = ConvertToUser(lite);
|
info.user = ConvertToUser(lite->num, lite->user);
|
||||||
}
|
}
|
||||||
if (deviceMetrics) {
|
if (lite->has_device_metrics) {
|
||||||
info.has_device_metrics = true;
|
info.has_device_metrics = true;
|
||||||
info.device_metrics = *deviceMetrics;
|
info.device_metrics = lite->device_metrics;
|
||||||
}
|
}
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite)
|
|
||||||
{
|
|
||||||
meshtastic_PositionLite posScratch;
|
|
||||||
meshtastic_DeviceMetrics dmScratch;
|
|
||||||
const meshtastic_PositionLite *pos = (nodeDB && nodeDB->copyNodePosition(lite->num, posScratch)) ? &posScratch : nullptr;
|
|
||||||
const meshtastic_DeviceMetrics *dm = (nodeDB && nodeDB->copyNodeTelemetry(lite->num, dmScratch)) ? &dmScratch : nullptr;
|
|
||||||
return ConvertToNodeInfo(lite, pos, dm);
|
|
||||||
}
|
|
||||||
|
|
||||||
meshtastic_NodeInfo TypeConversions::ConvertToNodeInfoThin(const meshtastic_NodeInfoLite *lite)
|
|
||||||
{
|
|
||||||
return ConvertToNodeInfo(lite, nullptr, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
meshtastic_PositionLite TypeConversions::ConvertToPositionLite(meshtastic_Position position)
|
meshtastic_PositionLite TypeConversions::ConvertToPositionLite(meshtastic_Position position)
|
||||||
{
|
{
|
||||||
meshtastic_PositionLite lite = meshtastic_PositionLite_init_default;
|
meshtastic_PositionLite lite = meshtastic_PositionLite_init_default;
|
||||||
@@ -93,50 +77,46 @@ meshtastic_Position TypeConversions::ConvertToPosition(meshtastic_PositionLite l
|
|||||||
return position;
|
return position;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TypeConversions::CopyUserToNodeInfoLite(meshtastic_NodeInfoLite *lite, const meshtastic_User &user)
|
meshtastic_UserLite TypeConversions::ConvertToUserLite(meshtastic_User user)
|
||||||
{
|
{
|
||||||
if (!lite)
|
meshtastic_UserLite lite = meshtastic_UserLite_init_default;
|
||||||
return;
|
|
||||||
|
|
||||||
strncpy(lite->long_name, user.long_name, sizeof(lite->long_name));
|
strncpy(lite.long_name, user.long_name, sizeof(lite.long_name));
|
||||||
lite->long_name[sizeof(lite->long_name) - 1] = '\0';
|
lite.long_name[sizeof(lite.long_name) - 1] = '\0';
|
||||||
sanitizeUtf8(lite->long_name, sizeof(lite->long_name));
|
sanitizeUtf8(lite.long_name, sizeof(lite.long_name));
|
||||||
strncpy(lite->short_name, user.short_name, sizeof(lite->short_name));
|
strncpy(lite.short_name, user.short_name, sizeof(lite.short_name));
|
||||||
lite->short_name[sizeof(lite->short_name) - 1] = '\0';
|
lite.short_name[sizeof(lite.short_name) - 1] = '\0';
|
||||||
sanitizeUtf8(lite->short_name, sizeof(lite->short_name));
|
sanitizeUtf8(lite.short_name, sizeof(lite.short_name));
|
||||||
lite->hw_model = user.hw_model;
|
lite.hw_model = user.hw_model;
|
||||||
lite->role = user.role;
|
lite.role = user.role;
|
||||||
memcpy(lite->public_key.bytes, user.public_key.bytes, sizeof(lite->public_key.bytes));
|
lite.is_licensed = user.is_licensed;
|
||||||
lite->public_key.size = user.public_key.size;
|
memcpy(lite.macaddr, user.macaddr, sizeof(lite.macaddr));
|
||||||
|
memcpy(lite.public_key.bytes, user.public_key.bytes, sizeof(lite.public_key.bytes));
|
||||||
nodeInfoLiteSetBit(lite, NODEINFO_BITFIELD_IS_LICENSED_MASK, user.is_licensed);
|
lite.public_key.size = user.public_key.size;
|
||||||
nodeInfoLiteSetBit(lite, NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK, user.has_is_unmessagable);
|
lite.has_is_unmessagable = user.has_is_unmessagable;
|
||||||
nodeInfoLiteSetBit(lite, NODEINFO_BITFIELD_IS_UNMESSAGABLE_MASK, user.has_is_unmessagable && user.is_unmessagable);
|
lite.is_unmessagable = user.is_unmessagable;
|
||||||
nodeInfoLiteSetBit(lite, NODEINFO_BITFIELD_HAS_USER_MASK, true);
|
return lite;
|
||||||
}
|
}
|
||||||
|
|
||||||
meshtastic_User TypeConversions::ConvertToUser(const meshtastic_NodeInfoLite *lite)
|
meshtastic_User TypeConversions::ConvertToUser(uint32_t nodeNum, meshtastic_UserLite lite)
|
||||||
{
|
{
|
||||||
meshtastic_User user = meshtastic_User_init_default;
|
meshtastic_User user = meshtastic_User_init_default;
|
||||||
if (!lite)
|
|
||||||
return user;
|
|
||||||
|
|
||||||
snprintf(user.id, sizeof(user.id), "!%08x", lite->num);
|
snprintf(user.id, sizeof(user.id), "!%08x", nodeNum);
|
||||||
strncpy(user.long_name, lite->long_name, sizeof(user.long_name));
|
strncpy(user.long_name, lite.long_name, sizeof(user.long_name));
|
||||||
user.long_name[sizeof(user.long_name) - 1] = '\0';
|
user.long_name[sizeof(user.long_name) - 1] = '\0';
|
||||||
sanitizeUtf8(user.long_name, sizeof(user.long_name));
|
sanitizeUtf8(user.long_name, sizeof(user.long_name));
|
||||||
strncpy(user.short_name, lite->short_name, sizeof(user.short_name));
|
strncpy(user.short_name, lite.short_name, sizeof(user.short_name));
|
||||||
user.short_name[sizeof(user.short_name) - 1] = '\0';
|
user.short_name[sizeof(user.short_name) - 1] = '\0';
|
||||||
sanitizeUtf8(user.short_name, sizeof(user.short_name));
|
sanitizeUtf8(user.short_name, sizeof(user.short_name));
|
||||||
user.hw_model = lite->hw_model;
|
user.hw_model = lite.hw_model;
|
||||||
user.role = lite->role;
|
user.role = lite.role;
|
||||||
user.is_licensed = nodeInfoLiteIsLicensed(lite);
|
user.is_licensed = lite.is_licensed;
|
||||||
memcpy(user.public_key.bytes, lite->public_key.bytes, sizeof(user.public_key.bytes));
|
memcpy(user.macaddr, lite.macaddr, sizeof(user.macaddr));
|
||||||
user.public_key.size = lite->public_key.size;
|
memcpy(user.public_key.bytes, lite.public_key.bytes, sizeof(user.public_key.bytes));
|
||||||
user.has_is_unmessagable = nodeInfoLiteHasIsUnmessagable(lite);
|
user.public_key.size = lite.public_key.size;
|
||||||
user.is_unmessagable = nodeInfoLiteIsUnmessagable(lite);
|
user.has_is_unmessagable = lite.has_is_unmessagable;
|
||||||
// macaddr is gone from the slim header; zero-fill for old clients that read it.
|
user.is_unmessagable = lite.is_unmessagable;
|
||||||
memset(user.macaddr, 0, sizeof(user.macaddr));
|
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
@@ -7,16 +7,9 @@
|
|||||||
class TypeConversions
|
class TypeConversions
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
// Either pointer may be null; the corresponding has_* fields stay unset.
|
|
||||||
static meshtastic_NodeInfo ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite, const meshtastic_PositionLite *position,
|
|
||||||
const meshtastic_DeviceMetrics *deviceMetrics);
|
|
||||||
static meshtastic_NodeInfo ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite);
|
static meshtastic_NodeInfo ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite);
|
||||||
// Identity + link-state only; satellite payloads are replayed afterward.
|
|
||||||
static meshtastic_NodeInfo ConvertToNodeInfoThin(const meshtastic_NodeInfoLite *lite);
|
|
||||||
|
|
||||||
static meshtastic_PositionLite ConvertToPositionLite(meshtastic_Position position);
|
static meshtastic_PositionLite ConvertToPositionLite(meshtastic_Position position);
|
||||||
static meshtastic_Position ConvertToPosition(meshtastic_PositionLite lite);
|
static meshtastic_Position ConvertToPosition(meshtastic_PositionLite lite);
|
||||||
|
static meshtastic_UserLite ConvertToUserLite(meshtastic_User user);
|
||||||
static void CopyUserToNodeInfoLite(meshtastic_NodeInfoLite *lite, const meshtastic_User &user);
|
static meshtastic_User ConvertToUser(uint32_t nodeNum, meshtastic_UserLite lite);
|
||||||
static meshtastic_User ConvertToUser(const meshtastic_NodeInfoLite *lite);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,18 +18,6 @@ PB_BIND(meshtastic_NodeInfoLite, meshtastic_NodeInfoLite, AUTO)
|
|||||||
PB_BIND(meshtastic_DeviceState, meshtastic_DeviceState, 2)
|
PB_BIND(meshtastic_DeviceState, meshtastic_DeviceState, 2)
|
||||||
|
|
||||||
|
|
||||||
PB_BIND(meshtastic_NodePositionEntry, meshtastic_NodePositionEntry, AUTO)
|
|
||||||
|
|
||||||
|
|
||||||
PB_BIND(meshtastic_NodeTelemetryEntry, meshtastic_NodeTelemetryEntry, AUTO)
|
|
||||||
|
|
||||||
|
|
||||||
PB_BIND(meshtastic_NodeEnvironmentEntry, meshtastic_NodeEnvironmentEntry, AUTO)
|
|
||||||
|
|
||||||
|
|
||||||
PB_BIND(meshtastic_NodeStatusEntry, meshtastic_NodeStatusEntry, AUTO)
|
|
||||||
|
|
||||||
|
|
||||||
PB_BIND(meshtastic_NodeDatabase, meshtastic_NodeDatabase, AUTO)
|
PB_BIND(meshtastic_NodeDatabase, meshtastic_NodeDatabase, AUTO)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -63,35 +63,43 @@ typedef struct _meshtastic_UserLite {
|
|||||||
bool is_unmessagable;
|
bool is_unmessagable;
|
||||||
} meshtastic_UserLite;
|
} meshtastic_UserLite;
|
||||||
|
|
||||||
typedef PB_BYTES_ARRAY_T(32) meshtastic_NodeInfoLite_public_key_t;
|
|
||||||
typedef struct _meshtastic_NodeInfoLite {
|
typedef struct _meshtastic_NodeInfoLite {
|
||||||
/* The node number */
|
/* The node number */
|
||||||
uint32_t num;
|
uint32_t num;
|
||||||
|
/* The user info for this node */
|
||||||
|
bool has_user;
|
||||||
|
meshtastic_UserLite user;
|
||||||
|
/* This position data. Note: before 1.2.14 we would also store the last time we've heard from this node in position.time, that is no longer true.
|
||||||
|
Position.time now indicates the last time we received a POSITION from that node. */
|
||||||
|
bool has_position;
|
||||||
|
meshtastic_PositionLite position;
|
||||||
/* Returns the Signal-to-noise ratio (SNR) of the last received message,
|
/* Returns the Signal-to-noise ratio (SNR) of the last received message,
|
||||||
as measured by the receiver. Return SNR of the last received message in dB */
|
as measured by the receiver. Return SNR of the last received message in dB */
|
||||||
float snr;
|
float snr;
|
||||||
/* Set to indicate the last time we received a packet from this node */
|
/* Set to indicate the last time we received a packet from this node */
|
||||||
uint32_t last_heard;
|
uint32_t last_heard;
|
||||||
|
/* The latest device metrics for the node. */
|
||||||
|
bool has_device_metrics;
|
||||||
|
meshtastic_DeviceMetrics device_metrics;
|
||||||
/* local channel index we heard that node on. Only populated if its not the default channel. */
|
/* local channel index we heard that node on. Only populated if its not the default channel. */
|
||||||
uint8_t channel;
|
uint8_t channel;
|
||||||
|
/* True if we witnessed the node over MQTT instead of LoRA transport */
|
||||||
|
bool via_mqtt;
|
||||||
/* Number of hops away from us this node is (0 if direct neighbor) */
|
/* Number of hops away from us this node is (0 if direct neighbor) */
|
||||||
bool has_hops_away;
|
bool has_hops_away;
|
||||||
uint8_t hops_away;
|
uint8_t hops_away;
|
||||||
|
/* True if node is in our favorites list
|
||||||
|
Persists between NodeDB internal clean ups */
|
||||||
|
bool is_favorite;
|
||||||
|
/* True if node is in our ignored list
|
||||||
|
Persists between NodeDB internal clean ups */
|
||||||
|
bool is_ignored;
|
||||||
/* Last byte of the node number of the node that should be used as the next hop to reach this node. */
|
/* Last byte of the node number of the node that should be used as the next hop to reach this node. */
|
||||||
uint8_t next_hop;
|
uint8_t next_hop;
|
||||||
/* Bitfield for storing booleans. See NODEINFO_BITFIELD_* in src/mesh/NodeDB.h. */
|
/* Bitfield for storing booleans.
|
||||||
|
LSB 0 is_key_manually_verified
|
||||||
|
LSB 1 is_muted */
|
||||||
uint32_t bitfield;
|
uint32_t bitfield;
|
||||||
/* A full name for this user, i.e. "Kevin Hester". */
|
|
||||||
char long_name[25];
|
|
||||||
/* A VERY short name, ideally two characters or an emoji.
|
|
||||||
Suitable for a tiny OLED screen. */
|
|
||||||
char short_name[5];
|
|
||||||
/* Hardware model the user's device is running. */
|
|
||||||
meshtastic_HardwareModel hw_model;
|
|
||||||
/* The user's role in the mesh. */
|
|
||||||
meshtastic_Config_DeviceConfig_Role role;
|
|
||||||
/* The public key of the user's device, for PKI-based encrypted DMs. */
|
|
||||||
meshtastic_NodeInfoLite_public_key_t public_key;
|
|
||||||
} meshtastic_NodeInfoLite;
|
} meshtastic_NodeInfoLite;
|
||||||
|
|
||||||
/* This message is never sent over the wire, but it is used for serializing DB
|
/* This message is never sent over the wire, but it is used for serializing DB
|
||||||
@@ -135,30 +143,6 @@ typedef struct _meshtastic_DeviceState {
|
|||||||
meshtastic_NodeRemoteHardwarePin node_remote_hardware_pins[12];
|
meshtastic_NodeRemoteHardwarePin node_remote_hardware_pins[12];
|
||||||
} meshtastic_DeviceState;
|
} meshtastic_DeviceState;
|
||||||
|
|
||||||
typedef struct _meshtastic_NodePositionEntry {
|
|
||||||
uint32_t num;
|
|
||||||
bool has_position;
|
|
||||||
meshtastic_PositionLite position;
|
|
||||||
} meshtastic_NodePositionEntry;
|
|
||||||
|
|
||||||
typedef struct _meshtastic_NodeTelemetryEntry {
|
|
||||||
uint32_t num;
|
|
||||||
bool has_device_metrics;
|
|
||||||
meshtastic_DeviceMetrics device_metrics;
|
|
||||||
} meshtastic_NodeTelemetryEntry;
|
|
||||||
|
|
||||||
typedef struct _meshtastic_NodeEnvironmentEntry {
|
|
||||||
uint32_t num;
|
|
||||||
bool has_environment_metrics;
|
|
||||||
meshtastic_EnvironmentMetrics environment_metrics;
|
|
||||||
} meshtastic_NodeEnvironmentEntry;
|
|
||||||
|
|
||||||
typedef struct _meshtastic_NodeStatusEntry {
|
|
||||||
uint32_t num;
|
|
||||||
bool has_status;
|
|
||||||
meshtastic_StatusMessage status;
|
|
||||||
} meshtastic_NodeStatusEntry;
|
|
||||||
|
|
||||||
typedef struct _meshtastic_NodeDatabase {
|
typedef struct _meshtastic_NodeDatabase {
|
||||||
/* A version integer used to invalidate old save files when we make
|
/* A version integer used to invalidate old save files when we make
|
||||||
incompatible changes This integer is set at build time and is private to
|
incompatible changes This integer is set at build time and is private to
|
||||||
@@ -166,12 +150,6 @@ typedef struct _meshtastic_NodeDatabase {
|
|||||||
uint32_t version;
|
uint32_t version;
|
||||||
/* New lite version of NodeDB to decrease memory footprint */
|
/* New lite version of NodeDB to decrease memory footprint */
|
||||||
std::vector<meshtastic_NodeInfoLite> nodes;
|
std::vector<meshtastic_NodeInfoLite> nodes;
|
||||||
/* Per-NodeNum satellite arrays. Constrained platforms (e.g. STM32WL) omit
|
|
||||||
these via MESHTASTIC_EXCLUDE_*DB build flags. */
|
|
||||||
std::vector<meshtastic_NodePositionEntry> positions;
|
|
||||||
std::vector<meshtastic_NodeTelemetryEntry> telemetry;
|
|
||||||
std::vector<meshtastic_NodeStatusEntry> status;
|
|
||||||
std::vector<meshtastic_NodeEnvironmentEntry> environment;
|
|
||||||
} meshtastic_NodeDatabase;
|
} meshtastic_NodeDatabase;
|
||||||
|
|
||||||
/* The on-disk saved channels */
|
/* The on-disk saved channels */
|
||||||
@@ -213,24 +191,16 @@ extern "C" {
|
|||||||
/* Initializer values for message structs */
|
/* Initializer values for message structs */
|
||||||
#define meshtastic_PositionLite_init_default {0, 0, 0, 0, _meshtastic_Position_LocSource_MIN}
|
#define meshtastic_PositionLite_init_default {0, 0, 0, 0, _meshtastic_Position_LocSource_MIN}
|
||||||
#define meshtastic_UserLite_init_default {{0}, "", "", _meshtastic_HardwareModel_MIN, 0, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}, false, 0}
|
#define meshtastic_UserLite_init_default {{0}, "", "", _meshtastic_HardwareModel_MIN, 0, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}, false, 0}
|
||||||
#define meshtastic_NodeInfoLite_init_default {0, 0, 0, 0, false, 0, 0, 0, "", "", _meshtastic_HardwareModel_MIN, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}}
|
#define meshtastic_NodeInfoLite_init_default {0, false, meshtastic_UserLite_init_default, false, meshtastic_PositionLite_init_default, 0, 0, false, meshtastic_DeviceMetrics_init_default, 0, 0, false, 0, 0, 0, 0, 0}
|
||||||
#define meshtastic_DeviceState_init_default {false, meshtastic_MyNodeInfo_init_default, false, meshtastic_User_init_default, 0, {meshtastic_MeshPacket_init_default}, false, meshtastic_MeshPacket_init_default, 0, 0, 0, false, meshtastic_MeshPacket_init_default, 0, {meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default}}
|
#define meshtastic_DeviceState_init_default {false, meshtastic_MyNodeInfo_init_default, false, meshtastic_User_init_default, 0, {meshtastic_MeshPacket_init_default}, false, meshtastic_MeshPacket_init_default, 0, 0, 0, false, meshtastic_MeshPacket_init_default, 0, {meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default}}
|
||||||
#define meshtastic_NodePositionEntry_init_default {0, false, meshtastic_PositionLite_init_default}
|
#define meshtastic_NodeDatabase_init_default {0, {0}}
|
||||||
#define meshtastic_NodeTelemetryEntry_init_default {0, false, meshtastic_DeviceMetrics_init_default}
|
|
||||||
#define meshtastic_NodeEnvironmentEntry_init_default {0, false, meshtastic_EnvironmentMetrics_init_default}
|
|
||||||
#define meshtastic_NodeStatusEntry_init_default {0, false, meshtastic_StatusMessage_init_default}
|
|
||||||
#define meshtastic_NodeDatabase_init_default {0, {0}, {0}, {0}, {0}, {0}}
|
|
||||||
#define meshtastic_ChannelFile_init_default {0, {meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default}, 0}
|
#define meshtastic_ChannelFile_init_default {0, {meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default}, 0}
|
||||||
#define meshtastic_BackupPreferences_init_default {0, 0, false, meshtastic_LocalConfig_init_default, false, meshtastic_LocalModuleConfig_init_default, false, meshtastic_ChannelFile_init_default, false, meshtastic_User_init_default}
|
#define meshtastic_BackupPreferences_init_default {0, 0, false, meshtastic_LocalConfig_init_default, false, meshtastic_LocalModuleConfig_init_default, false, meshtastic_ChannelFile_init_default, false, meshtastic_User_init_default}
|
||||||
#define meshtastic_PositionLite_init_zero {0, 0, 0, 0, _meshtastic_Position_LocSource_MIN}
|
#define meshtastic_PositionLite_init_zero {0, 0, 0, 0, _meshtastic_Position_LocSource_MIN}
|
||||||
#define meshtastic_UserLite_init_zero {{0}, "", "", _meshtastic_HardwareModel_MIN, 0, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}, false, 0}
|
#define meshtastic_UserLite_init_zero {{0}, "", "", _meshtastic_HardwareModel_MIN, 0, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}, false, 0}
|
||||||
#define meshtastic_NodeInfoLite_init_zero {0, 0, 0, 0, false, 0, 0, 0, "", "", _meshtastic_HardwareModel_MIN, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}}
|
#define meshtastic_NodeInfoLite_init_zero {0, false, meshtastic_UserLite_init_zero, false, meshtastic_PositionLite_init_zero, 0, 0, false, meshtastic_DeviceMetrics_init_zero, 0, 0, false, 0, 0, 0, 0, 0}
|
||||||
#define meshtastic_DeviceState_init_zero {false, meshtastic_MyNodeInfo_init_zero, false, meshtastic_User_init_zero, 0, {meshtastic_MeshPacket_init_zero}, false, meshtastic_MeshPacket_init_zero, 0, 0, 0, false, meshtastic_MeshPacket_init_zero, 0, {meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero}}
|
#define meshtastic_DeviceState_init_zero {false, meshtastic_MyNodeInfo_init_zero, false, meshtastic_User_init_zero, 0, {meshtastic_MeshPacket_init_zero}, false, meshtastic_MeshPacket_init_zero, 0, 0, 0, false, meshtastic_MeshPacket_init_zero, 0, {meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero}}
|
||||||
#define meshtastic_NodePositionEntry_init_zero {0, false, meshtastic_PositionLite_init_zero}
|
#define meshtastic_NodeDatabase_init_zero {0, {0}}
|
||||||
#define meshtastic_NodeTelemetryEntry_init_zero {0, false, meshtastic_DeviceMetrics_init_zero}
|
|
||||||
#define meshtastic_NodeEnvironmentEntry_init_zero {0, false, meshtastic_EnvironmentMetrics_init_zero}
|
|
||||||
#define meshtastic_NodeStatusEntry_init_zero {0, false, meshtastic_StatusMessage_init_zero}
|
|
||||||
#define meshtastic_NodeDatabase_init_zero {0, {0}, {0}, {0}, {0}, {0}}
|
|
||||||
#define meshtastic_ChannelFile_init_zero {0, {meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero}, 0}
|
#define meshtastic_ChannelFile_init_zero {0, {meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero}, 0}
|
||||||
#define meshtastic_BackupPreferences_init_zero {0, 0, false, meshtastic_LocalConfig_init_zero, false, meshtastic_LocalModuleConfig_init_zero, false, meshtastic_ChannelFile_init_zero, false, meshtastic_User_init_zero}
|
#define meshtastic_BackupPreferences_init_zero {0, 0, false, meshtastic_LocalConfig_init_zero, false, meshtastic_LocalModuleConfig_init_zero, false, meshtastic_ChannelFile_init_zero, false, meshtastic_User_init_zero}
|
||||||
|
|
||||||
@@ -249,17 +219,18 @@ extern "C" {
|
|||||||
#define meshtastic_UserLite_public_key_tag 7
|
#define meshtastic_UserLite_public_key_tag 7
|
||||||
#define meshtastic_UserLite_is_unmessagable_tag 9
|
#define meshtastic_UserLite_is_unmessagable_tag 9
|
||||||
#define meshtastic_NodeInfoLite_num_tag 1
|
#define meshtastic_NodeInfoLite_num_tag 1
|
||||||
|
#define meshtastic_NodeInfoLite_user_tag 2
|
||||||
|
#define meshtastic_NodeInfoLite_position_tag 3
|
||||||
#define meshtastic_NodeInfoLite_snr_tag 4
|
#define meshtastic_NodeInfoLite_snr_tag 4
|
||||||
#define meshtastic_NodeInfoLite_last_heard_tag 5
|
#define meshtastic_NodeInfoLite_last_heard_tag 5
|
||||||
|
#define meshtastic_NodeInfoLite_device_metrics_tag 6
|
||||||
#define meshtastic_NodeInfoLite_channel_tag 7
|
#define meshtastic_NodeInfoLite_channel_tag 7
|
||||||
|
#define meshtastic_NodeInfoLite_via_mqtt_tag 8
|
||||||
#define meshtastic_NodeInfoLite_hops_away_tag 9
|
#define meshtastic_NodeInfoLite_hops_away_tag 9
|
||||||
|
#define meshtastic_NodeInfoLite_is_favorite_tag 10
|
||||||
|
#define meshtastic_NodeInfoLite_is_ignored_tag 11
|
||||||
#define meshtastic_NodeInfoLite_next_hop_tag 12
|
#define meshtastic_NodeInfoLite_next_hop_tag 12
|
||||||
#define meshtastic_NodeInfoLite_bitfield_tag 13
|
#define meshtastic_NodeInfoLite_bitfield_tag 13
|
||||||
#define meshtastic_NodeInfoLite_long_name_tag 14
|
|
||||||
#define meshtastic_NodeInfoLite_short_name_tag 15
|
|
||||||
#define meshtastic_NodeInfoLite_hw_model_tag 16
|
|
||||||
#define meshtastic_NodeInfoLite_role_tag 17
|
|
||||||
#define meshtastic_NodeInfoLite_public_key_tag 18
|
|
||||||
#define meshtastic_DeviceState_my_node_tag 2
|
#define meshtastic_DeviceState_my_node_tag 2
|
||||||
#define meshtastic_DeviceState_owner_tag 3
|
#define meshtastic_DeviceState_owner_tag 3
|
||||||
#define meshtastic_DeviceState_receive_queue_tag 5
|
#define meshtastic_DeviceState_receive_queue_tag 5
|
||||||
@@ -269,20 +240,8 @@ extern "C" {
|
|||||||
#define meshtastic_DeviceState_did_gps_reset_tag 11
|
#define meshtastic_DeviceState_did_gps_reset_tag 11
|
||||||
#define meshtastic_DeviceState_rx_waypoint_tag 12
|
#define meshtastic_DeviceState_rx_waypoint_tag 12
|
||||||
#define meshtastic_DeviceState_node_remote_hardware_pins_tag 13
|
#define meshtastic_DeviceState_node_remote_hardware_pins_tag 13
|
||||||
#define meshtastic_NodePositionEntry_num_tag 1
|
|
||||||
#define meshtastic_NodePositionEntry_position_tag 2
|
|
||||||
#define meshtastic_NodeTelemetryEntry_num_tag 1
|
|
||||||
#define meshtastic_NodeTelemetryEntry_device_metrics_tag 2
|
|
||||||
#define meshtastic_NodeEnvironmentEntry_num_tag 1
|
|
||||||
#define meshtastic_NodeEnvironmentEntry_environment_metrics_tag 2
|
|
||||||
#define meshtastic_NodeStatusEntry_num_tag 1
|
|
||||||
#define meshtastic_NodeStatusEntry_status_tag 2
|
|
||||||
#define meshtastic_NodeDatabase_version_tag 1
|
#define meshtastic_NodeDatabase_version_tag 1
|
||||||
#define meshtastic_NodeDatabase_nodes_tag 2
|
#define meshtastic_NodeDatabase_nodes_tag 2
|
||||||
#define meshtastic_NodeDatabase_positions_tag 3
|
|
||||||
#define meshtastic_NodeDatabase_telemetry_tag 4
|
|
||||||
#define meshtastic_NodeDatabase_status_tag 5
|
|
||||||
#define meshtastic_NodeDatabase_environment_tag 6
|
|
||||||
#define meshtastic_ChannelFile_channels_tag 1
|
#define meshtastic_ChannelFile_channels_tag 1
|
||||||
#define meshtastic_ChannelFile_version_tag 2
|
#define meshtastic_ChannelFile_version_tag 2
|
||||||
#define meshtastic_BackupPreferences_version_tag 1
|
#define meshtastic_BackupPreferences_version_tag 1
|
||||||
@@ -316,19 +275,23 @@ X(a, STATIC, OPTIONAL, BOOL, is_unmessagable, 9)
|
|||||||
|
|
||||||
#define meshtastic_NodeInfoLite_FIELDLIST(X, a) \
|
#define meshtastic_NodeInfoLite_FIELDLIST(X, a) \
|
||||||
X(a, STATIC, SINGULAR, UINT32, num, 1) \
|
X(a, STATIC, SINGULAR, UINT32, num, 1) \
|
||||||
|
X(a, STATIC, OPTIONAL, MESSAGE, user, 2) \
|
||||||
|
X(a, STATIC, OPTIONAL, MESSAGE, position, 3) \
|
||||||
X(a, STATIC, SINGULAR, FLOAT, snr, 4) \
|
X(a, STATIC, SINGULAR, FLOAT, snr, 4) \
|
||||||
X(a, STATIC, SINGULAR, FIXED32, last_heard, 5) \
|
X(a, STATIC, SINGULAR, FIXED32, last_heard, 5) \
|
||||||
|
X(a, STATIC, OPTIONAL, MESSAGE, device_metrics, 6) \
|
||||||
X(a, STATIC, SINGULAR, UINT32, channel, 7) \
|
X(a, STATIC, SINGULAR, UINT32, channel, 7) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, via_mqtt, 8) \
|
||||||
X(a, STATIC, OPTIONAL, UINT32, hops_away, 9) \
|
X(a, STATIC, OPTIONAL, UINT32, hops_away, 9) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, is_favorite, 10) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, is_ignored, 11) \
|
||||||
X(a, STATIC, SINGULAR, UINT32, next_hop, 12) \
|
X(a, STATIC, SINGULAR, UINT32, next_hop, 12) \
|
||||||
X(a, STATIC, SINGULAR, UINT32, bitfield, 13) \
|
X(a, STATIC, SINGULAR, UINT32, bitfield, 13)
|
||||||
X(a, STATIC, SINGULAR, STRING, long_name, 14) \
|
|
||||||
X(a, STATIC, SINGULAR, STRING, short_name, 15) \
|
|
||||||
X(a, STATIC, SINGULAR, UENUM, hw_model, 16) \
|
|
||||||
X(a, STATIC, SINGULAR, UENUM, role, 17) \
|
|
||||||
X(a, STATIC, SINGULAR, BYTES, public_key, 18)
|
|
||||||
#define meshtastic_NodeInfoLite_CALLBACK NULL
|
#define meshtastic_NodeInfoLite_CALLBACK NULL
|
||||||
#define meshtastic_NodeInfoLite_DEFAULT NULL
|
#define meshtastic_NodeInfoLite_DEFAULT NULL
|
||||||
|
#define meshtastic_NodeInfoLite_user_MSGTYPE meshtastic_UserLite
|
||||||
|
#define meshtastic_NodeInfoLite_position_MSGTYPE meshtastic_PositionLite
|
||||||
|
#define meshtastic_NodeInfoLite_device_metrics_MSGTYPE meshtastic_DeviceMetrics
|
||||||
|
|
||||||
#define meshtastic_DeviceState_FIELDLIST(X, a) \
|
#define meshtastic_DeviceState_FIELDLIST(X, a) \
|
||||||
X(a, STATIC, OPTIONAL, MESSAGE, my_node, 2) \
|
X(a, STATIC, OPTIONAL, MESSAGE, my_node, 2) \
|
||||||
@@ -349,49 +312,13 @@ X(a, STATIC, REPEATED, MESSAGE, node_remote_hardware_pins, 13)
|
|||||||
#define meshtastic_DeviceState_rx_waypoint_MSGTYPE meshtastic_MeshPacket
|
#define meshtastic_DeviceState_rx_waypoint_MSGTYPE meshtastic_MeshPacket
|
||||||
#define meshtastic_DeviceState_node_remote_hardware_pins_MSGTYPE meshtastic_NodeRemoteHardwarePin
|
#define meshtastic_DeviceState_node_remote_hardware_pins_MSGTYPE meshtastic_NodeRemoteHardwarePin
|
||||||
|
|
||||||
#define meshtastic_NodePositionEntry_FIELDLIST(X, a) \
|
|
||||||
X(a, STATIC, SINGULAR, UINT32, num, 1) \
|
|
||||||
X(a, STATIC, OPTIONAL, MESSAGE, position, 2)
|
|
||||||
#define meshtastic_NodePositionEntry_CALLBACK NULL
|
|
||||||
#define meshtastic_NodePositionEntry_DEFAULT NULL
|
|
||||||
#define meshtastic_NodePositionEntry_position_MSGTYPE meshtastic_PositionLite
|
|
||||||
|
|
||||||
#define meshtastic_NodeTelemetryEntry_FIELDLIST(X, a) \
|
|
||||||
X(a, STATIC, SINGULAR, UINT32, num, 1) \
|
|
||||||
X(a, STATIC, OPTIONAL, MESSAGE, device_metrics, 2)
|
|
||||||
#define meshtastic_NodeTelemetryEntry_CALLBACK NULL
|
|
||||||
#define meshtastic_NodeTelemetryEntry_DEFAULT NULL
|
|
||||||
#define meshtastic_NodeTelemetryEntry_device_metrics_MSGTYPE meshtastic_DeviceMetrics
|
|
||||||
|
|
||||||
#define meshtastic_NodeEnvironmentEntry_FIELDLIST(X, a) \
|
|
||||||
X(a, STATIC, SINGULAR, UINT32, num, 1) \
|
|
||||||
X(a, STATIC, OPTIONAL, MESSAGE, environment_metrics, 2)
|
|
||||||
#define meshtastic_NodeEnvironmentEntry_CALLBACK NULL
|
|
||||||
#define meshtastic_NodeEnvironmentEntry_DEFAULT NULL
|
|
||||||
#define meshtastic_NodeEnvironmentEntry_environment_metrics_MSGTYPE meshtastic_EnvironmentMetrics
|
|
||||||
|
|
||||||
#define meshtastic_NodeStatusEntry_FIELDLIST(X, a) \
|
|
||||||
X(a, STATIC, SINGULAR, UINT32, num, 1) \
|
|
||||||
X(a, STATIC, OPTIONAL, MESSAGE, status, 2)
|
|
||||||
#define meshtastic_NodeStatusEntry_CALLBACK NULL
|
|
||||||
#define meshtastic_NodeStatusEntry_DEFAULT NULL
|
|
||||||
#define meshtastic_NodeStatusEntry_status_MSGTYPE meshtastic_StatusMessage
|
|
||||||
|
|
||||||
#define meshtastic_NodeDatabase_FIELDLIST(X, a) \
|
#define meshtastic_NodeDatabase_FIELDLIST(X, a) \
|
||||||
X(a, STATIC, SINGULAR, UINT32, version, 1) \
|
X(a, STATIC, SINGULAR, UINT32, version, 1) \
|
||||||
X(a, CALLBACK, REPEATED, MESSAGE, nodes, 2) \
|
X(a, CALLBACK, REPEATED, MESSAGE, nodes, 2)
|
||||||
X(a, CALLBACK, REPEATED, MESSAGE, positions, 3) \
|
|
||||||
X(a, CALLBACK, REPEATED, MESSAGE, telemetry, 4) \
|
|
||||||
X(a, CALLBACK, REPEATED, MESSAGE, status, 5) \
|
|
||||||
X(a, CALLBACK, REPEATED, MESSAGE, environment, 6)
|
|
||||||
extern bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_t *field);
|
extern bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_t *field);
|
||||||
#define meshtastic_NodeDatabase_CALLBACK meshtastic_NodeDatabase_callback
|
#define meshtastic_NodeDatabase_CALLBACK meshtastic_NodeDatabase_callback
|
||||||
#define meshtastic_NodeDatabase_DEFAULT NULL
|
#define meshtastic_NodeDatabase_DEFAULT NULL
|
||||||
#define meshtastic_NodeDatabase_nodes_MSGTYPE meshtastic_NodeInfoLite
|
#define meshtastic_NodeDatabase_nodes_MSGTYPE meshtastic_NodeInfoLite
|
||||||
#define meshtastic_NodeDatabase_positions_MSGTYPE meshtastic_NodePositionEntry
|
|
||||||
#define meshtastic_NodeDatabase_telemetry_MSGTYPE meshtastic_NodeTelemetryEntry
|
|
||||||
#define meshtastic_NodeDatabase_status_MSGTYPE meshtastic_NodeStatusEntry
|
|
||||||
#define meshtastic_NodeDatabase_environment_MSGTYPE meshtastic_NodeEnvironmentEntry
|
|
||||||
|
|
||||||
#define meshtastic_ChannelFile_FIELDLIST(X, a) \
|
#define meshtastic_ChannelFile_FIELDLIST(X, a) \
|
||||||
X(a, STATIC, REPEATED, MESSAGE, channels, 1) \
|
X(a, STATIC, REPEATED, MESSAGE, channels, 1) \
|
||||||
@@ -418,10 +345,6 @@ extern const pb_msgdesc_t meshtastic_PositionLite_msg;
|
|||||||
extern const pb_msgdesc_t meshtastic_UserLite_msg;
|
extern const pb_msgdesc_t meshtastic_UserLite_msg;
|
||||||
extern const pb_msgdesc_t meshtastic_NodeInfoLite_msg;
|
extern const pb_msgdesc_t meshtastic_NodeInfoLite_msg;
|
||||||
extern const pb_msgdesc_t meshtastic_DeviceState_msg;
|
extern const pb_msgdesc_t meshtastic_DeviceState_msg;
|
||||||
extern const pb_msgdesc_t meshtastic_NodePositionEntry_msg;
|
|
||||||
extern const pb_msgdesc_t meshtastic_NodeTelemetryEntry_msg;
|
|
||||||
extern const pb_msgdesc_t meshtastic_NodeEnvironmentEntry_msg;
|
|
||||||
extern const pb_msgdesc_t meshtastic_NodeStatusEntry_msg;
|
|
||||||
extern const pb_msgdesc_t meshtastic_NodeDatabase_msg;
|
extern const pb_msgdesc_t meshtastic_NodeDatabase_msg;
|
||||||
extern const pb_msgdesc_t meshtastic_ChannelFile_msg;
|
extern const pb_msgdesc_t meshtastic_ChannelFile_msg;
|
||||||
extern const pb_msgdesc_t meshtastic_BackupPreferences_msg;
|
extern const pb_msgdesc_t meshtastic_BackupPreferences_msg;
|
||||||
@@ -431,10 +354,6 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg;
|
|||||||
#define meshtastic_UserLite_fields &meshtastic_UserLite_msg
|
#define meshtastic_UserLite_fields &meshtastic_UserLite_msg
|
||||||
#define meshtastic_NodeInfoLite_fields &meshtastic_NodeInfoLite_msg
|
#define meshtastic_NodeInfoLite_fields &meshtastic_NodeInfoLite_msg
|
||||||
#define meshtastic_DeviceState_fields &meshtastic_DeviceState_msg
|
#define meshtastic_DeviceState_fields &meshtastic_DeviceState_msg
|
||||||
#define meshtastic_NodePositionEntry_fields &meshtastic_NodePositionEntry_msg
|
|
||||||
#define meshtastic_NodeTelemetryEntry_fields &meshtastic_NodeTelemetryEntry_msg
|
|
||||||
#define meshtastic_NodeEnvironmentEntry_fields &meshtastic_NodeEnvironmentEntry_msg
|
|
||||||
#define meshtastic_NodeStatusEntry_fields &meshtastic_NodeStatusEntry_msg
|
|
||||||
#define meshtastic_NodeDatabase_fields &meshtastic_NodeDatabase_msg
|
#define meshtastic_NodeDatabase_fields &meshtastic_NodeDatabase_msg
|
||||||
#define meshtastic_ChannelFile_fields &meshtastic_ChannelFile_msg
|
#define meshtastic_ChannelFile_fields &meshtastic_ChannelFile_msg
|
||||||
#define meshtastic_BackupPreferences_fields &meshtastic_BackupPreferences_msg
|
#define meshtastic_BackupPreferences_fields &meshtastic_BackupPreferences_msg
|
||||||
@@ -445,11 +364,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg;
|
|||||||
#define meshtastic_BackupPreferences_size 2432
|
#define meshtastic_BackupPreferences_size 2432
|
||||||
#define meshtastic_ChannelFile_size 718
|
#define meshtastic_ChannelFile_size 718
|
||||||
#define meshtastic_DeviceState_size 1737
|
#define meshtastic_DeviceState_size 1737
|
||||||
#define meshtastic_NodeEnvironmentEntry_size 170
|
#define meshtastic_NodeInfoLite_size 196
|
||||||
#define meshtastic_NodeInfoLite_size 105
|
|
||||||
#define meshtastic_NodePositionEntry_size 36
|
|
||||||
#define meshtastic_NodeStatusEntry_size 89
|
|
||||||
#define meshtastic_NodeTelemetryEntry_size 35
|
|
||||||
#define meshtastic_PositionLite_size 28
|
#define meshtastic_PositionLite_size 28
|
||||||
#define meshtastic_UserLite_size 98
|
#define meshtastic_UserLite_size 98
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
/* Automatically generated nanopb constant definitions */
|
|
||||||
/* Generated by nanopb-0.4.9.1 */
|
|
||||||
|
|
||||||
#include "meshtastic/deviceonly_legacy.pb.h"
|
|
||||||
#if PB_PROTO_HEADER_VERSION != 40
|
|
||||||
#error Regenerate this file with the current version of nanopb generator.
|
|
||||||
#endif
|
|
||||||
|
|
||||||
PB_BIND(meshtastic_NodeInfoLite_Legacy, meshtastic_NodeInfoLite_Legacy, AUTO)
|
|
||||||
|
|
||||||
|
|
||||||
PB_BIND(meshtastic_NodeDatabase_Legacy, meshtastic_NodeDatabase_Legacy, AUTO)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
/* Automatically generated nanopb header */
|
|
||||||
/* Generated by nanopb-0.4.9.1 */
|
|
||||||
|
|
||||||
#ifndef PB_MESHTASTIC_MESHTASTIC_DEVICEONLY_LEGACY_PB_H_INCLUDED
|
|
||||||
#define PB_MESHTASTIC_MESHTASTIC_DEVICEONLY_LEGACY_PB_H_INCLUDED
|
|
||||||
#include <pb.h>
|
|
||||||
#include <vector>
|
|
||||||
#include "meshtastic/deviceonly.pb.h"
|
|
||||||
#include "meshtastic/telemetry.pb.h"
|
|
||||||
|
|
||||||
#if PB_PROTO_HEADER_VERSION != 40
|
|
||||||
#error Regenerate this file with the current version of nanopb generator.
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* Struct definitions */
|
|
||||||
/* Legacy NodeInfoLite descriptor used only to decode pre-split
|
|
||||||
/prefs/nodes.proto saves during the v24 -> v25 migration boot.
|
|
||||||
This preserves the original NodeInfoLite-compatible field numbers needed
|
|
||||||
to parse old wire bytes cleanly, including user (2), position (3),
|
|
||||||
device_metrics (6), and the legacy-only compatibility fields via_mqtt (8),
|
|
||||||
is_favorite (10), and is_ignored (11). Steady-state code does not use
|
|
||||||
this struct; it is dropped after migration completes. This file should be
|
|
||||||
removed once DEVICESTATE_MIN_VER advances past 24. */
|
|
||||||
typedef struct _meshtastic_NodeInfoLite_Legacy {
|
|
||||||
uint32_t num;
|
|
||||||
bool has_user;
|
|
||||||
meshtastic_UserLite user;
|
|
||||||
bool has_position;
|
|
||||||
meshtastic_PositionLite position;
|
|
||||||
float snr;
|
|
||||||
uint32_t last_heard;
|
|
||||||
bool has_device_metrics;
|
|
||||||
meshtastic_DeviceMetrics device_metrics;
|
|
||||||
uint8_t channel;
|
|
||||||
bool via_mqtt;
|
|
||||||
bool has_hops_away;
|
|
||||||
uint8_t hops_away;
|
|
||||||
bool is_favorite;
|
|
||||||
bool is_ignored;
|
|
||||||
uint8_t next_hop;
|
|
||||||
uint32_t bitfield;
|
|
||||||
} meshtastic_NodeInfoLite_Legacy;
|
|
||||||
|
|
||||||
/* Legacy NodeDatabase shape: one repeated array of fat NodeInfoLite_Legacy
|
|
||||||
with no satellite position/telemetry arrays. */
|
|
||||||
typedef struct _meshtastic_NodeDatabase_Legacy {
|
|
||||||
uint32_t version;
|
|
||||||
std::vector<meshtastic_NodeInfoLite_Legacy> nodes;
|
|
||||||
} meshtastic_NodeDatabase_Legacy;
|
|
||||||
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* Initializer values for message structs */
|
|
||||||
#define meshtastic_NodeInfoLite_Legacy_init_default {0, false, meshtastic_UserLite_init_default, false, meshtastic_PositionLite_init_default, 0, 0, false, meshtastic_DeviceMetrics_init_default, 0, 0, false, 0, 0, 0, 0, 0}
|
|
||||||
#define meshtastic_NodeDatabase_Legacy_init_default {0, {0}}
|
|
||||||
#define meshtastic_NodeInfoLite_Legacy_init_zero {0, false, meshtastic_UserLite_init_zero, false, meshtastic_PositionLite_init_zero, 0, 0, false, meshtastic_DeviceMetrics_init_zero, 0, 0, false, 0, 0, 0, 0, 0}
|
|
||||||
#define meshtastic_NodeDatabase_Legacy_init_zero {0, {0}}
|
|
||||||
|
|
||||||
/* Field tags (for use in manual encoding/decoding) */
|
|
||||||
#define meshtastic_NodeInfoLite_Legacy_num_tag 1
|
|
||||||
#define meshtastic_NodeInfoLite_Legacy_user_tag 2
|
|
||||||
#define meshtastic_NodeInfoLite_Legacy_position_tag 3
|
|
||||||
#define meshtastic_NodeInfoLite_Legacy_snr_tag 4
|
|
||||||
#define meshtastic_NodeInfoLite_Legacy_last_heard_tag 5
|
|
||||||
#define meshtastic_NodeInfoLite_Legacy_device_metrics_tag 6
|
|
||||||
#define meshtastic_NodeInfoLite_Legacy_channel_tag 7
|
|
||||||
#define meshtastic_NodeInfoLite_Legacy_via_mqtt_tag 8
|
|
||||||
#define meshtastic_NodeInfoLite_Legacy_hops_away_tag 9
|
|
||||||
#define meshtastic_NodeInfoLite_Legacy_is_favorite_tag 10
|
|
||||||
#define meshtastic_NodeInfoLite_Legacy_is_ignored_tag 11
|
|
||||||
#define meshtastic_NodeInfoLite_Legacy_next_hop_tag 12
|
|
||||||
#define meshtastic_NodeInfoLite_Legacy_bitfield_tag 13
|
|
||||||
#define meshtastic_NodeDatabase_Legacy_version_tag 1
|
|
||||||
#define meshtastic_NodeDatabase_Legacy_nodes_tag 2
|
|
||||||
|
|
||||||
/* Struct field encoding specification for nanopb */
|
|
||||||
#define meshtastic_NodeInfoLite_Legacy_FIELDLIST(X, a) \
|
|
||||||
X(a, STATIC, SINGULAR, UINT32, num, 1) \
|
|
||||||
X(a, STATIC, OPTIONAL, MESSAGE, user, 2) \
|
|
||||||
X(a, STATIC, OPTIONAL, MESSAGE, position, 3) \
|
|
||||||
X(a, STATIC, SINGULAR, FLOAT, snr, 4) \
|
|
||||||
X(a, STATIC, SINGULAR, FIXED32, last_heard, 5) \
|
|
||||||
X(a, STATIC, OPTIONAL, MESSAGE, device_metrics, 6) \
|
|
||||||
X(a, STATIC, SINGULAR, UINT32, channel, 7) \
|
|
||||||
X(a, STATIC, SINGULAR, BOOL, via_mqtt, 8) \
|
|
||||||
X(a, STATIC, OPTIONAL, UINT32, hops_away, 9) \
|
|
||||||
X(a, STATIC, SINGULAR, BOOL, is_favorite, 10) \
|
|
||||||
X(a, STATIC, SINGULAR, BOOL, is_ignored, 11) \
|
|
||||||
X(a, STATIC, SINGULAR, UINT32, next_hop, 12) \
|
|
||||||
X(a, STATIC, SINGULAR, UINT32, bitfield, 13)
|
|
||||||
#define meshtastic_NodeInfoLite_Legacy_CALLBACK NULL
|
|
||||||
#define meshtastic_NodeInfoLite_Legacy_DEFAULT NULL
|
|
||||||
#define meshtastic_NodeInfoLite_Legacy_user_MSGTYPE meshtastic_UserLite
|
|
||||||
#define meshtastic_NodeInfoLite_Legacy_position_MSGTYPE meshtastic_PositionLite
|
|
||||||
#define meshtastic_NodeInfoLite_Legacy_device_metrics_MSGTYPE meshtastic_DeviceMetrics
|
|
||||||
|
|
||||||
#define meshtastic_NodeDatabase_Legacy_FIELDLIST(X, a) \
|
|
||||||
X(a, STATIC, SINGULAR, UINT32, version, 1) \
|
|
||||||
X(a, CALLBACK, REPEATED, MESSAGE, nodes, 2)
|
|
||||||
extern bool meshtastic_NodeDatabase_Legacy_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_t *field);
|
|
||||||
#define meshtastic_NodeDatabase_Legacy_CALLBACK meshtastic_NodeDatabase_Legacy_callback
|
|
||||||
#define meshtastic_NodeDatabase_Legacy_DEFAULT NULL
|
|
||||||
#define meshtastic_NodeDatabase_Legacy_nodes_MSGTYPE meshtastic_NodeInfoLite_Legacy
|
|
||||||
|
|
||||||
extern const pb_msgdesc_t meshtastic_NodeInfoLite_Legacy_msg;
|
|
||||||
extern const pb_msgdesc_t meshtastic_NodeDatabase_Legacy_msg;
|
|
||||||
|
|
||||||
/* Defines for backwards compatibility with code written before nanopb-0.4.0 */
|
|
||||||
#define meshtastic_NodeInfoLite_Legacy_fields &meshtastic_NodeInfoLite_Legacy_msg
|
|
||||||
#define meshtastic_NodeDatabase_Legacy_fields &meshtastic_NodeDatabase_Legacy_msg
|
|
||||||
|
|
||||||
/* Maximum encoded size of messages (where known) */
|
|
||||||
/* meshtastic_NodeDatabase_Legacy_size depends on runtime parameters */
|
|
||||||
#define MESHTASTIC_MESHTASTIC_DEVICEONLY_LEGACY_PB_H_MAX_SIZE meshtastic_NodeInfoLite_Legacy_size
|
|
||||||
#define meshtastic_NodeInfoLite_Legacy_size 196
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
} /* extern "C" */
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -729,7 +729,7 @@ void handleNodes(HTTPRequest *req, HTTPResponse *res)
|
|||||||
uint32_t readIndex = 0;
|
uint32_t readIndex = 0;
|
||||||
const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
|
const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
|
||||||
while (tempNodeInfo != NULL) {
|
while (tempNodeInfo != NULL) {
|
||||||
if (nodeInfoLiteHasUser(tempNodeInfo)) {
|
if (tempNodeInfo->has_user) {
|
||||||
JSONObject node;
|
JSONObject node;
|
||||||
|
|
||||||
char id[16];
|
char id[16];
|
||||||
@@ -737,26 +737,26 @@ void handleNodes(HTTPRequest *req, HTTPResponse *res)
|
|||||||
|
|
||||||
node["id"] = new JSONValue(id);
|
node["id"] = new JSONValue(id);
|
||||||
node["snr"] = new JSONValue(tempNodeInfo->snr);
|
node["snr"] = new JSONValue(tempNodeInfo->snr);
|
||||||
node["via_mqtt"] = new JSONValue(BoolToString(nodeInfoLiteViaMqtt(tempNodeInfo)));
|
node["via_mqtt"] = new JSONValue(BoolToString(tempNodeInfo->via_mqtt));
|
||||||
node["last_heard"] = new JSONValue((int)tempNodeInfo->last_heard);
|
node["last_heard"] = new JSONValue((int)tempNodeInfo->last_heard);
|
||||||
node["position"] = new JSONValue();
|
node["position"] = new JSONValue();
|
||||||
|
|
||||||
if (nodeDB->hasValidPosition(tempNodeInfo)) {
|
if (nodeDB->hasValidPosition(tempNodeInfo)) {
|
||||||
meshtastic_PositionLite posLite;
|
JSONObject position;
|
||||||
if (nodeDB->copyNodePosition(tempNodeInfo->num, posLite)) {
|
position["latitude"] = new JSONValue((float)tempNodeInfo->position.latitude_i * 1e-7);
|
||||||
JSONObject position;
|
position["longitude"] = new JSONValue((float)tempNodeInfo->position.longitude_i * 1e-7);
|
||||||
position["latitude"] = new JSONValue((float)posLite.latitude_i * 1e-7);
|
position["altitude"] = new JSONValue((int)tempNodeInfo->position.altitude);
|
||||||
position["longitude"] = new JSONValue((float)posLite.longitude_i * 1e-7);
|
node["position"] = new JSONValue(position);
|
||||||
position["altitude"] = new JSONValue((int)posLite.altitude);
|
|
||||||
node["position"] = new JSONValue(position);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
node["long_name"] = new JSONValue(tempNodeInfo->long_name);
|
node["long_name"] = new JSONValue(tempNodeInfo->user.long_name);
|
||||||
node["short_name"] = new JSONValue(tempNodeInfo->short_name);
|
node["short_name"] = new JSONValue(tempNodeInfo->user.short_name);
|
||||||
// mac_address dropped from NodeInfoLite as part of the slim refactor; emit zeros.
|
char macStr[18];
|
||||||
node["mac_address"] = new JSONValue("00:00:00:00:00:00");
|
snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X", tempNodeInfo->user.macaddr[0],
|
||||||
node["hw_model"] = new JSONValue(tempNodeInfo->hw_model);
|
tempNodeInfo->user.macaddr[1], tempNodeInfo->user.macaddr[2], tempNodeInfo->user.macaddr[3],
|
||||||
|
tempNodeInfo->user.macaddr[4], tempNodeInfo->user.macaddr[5]);
|
||||||
|
node["mac_address"] = new JSONValue(macStr);
|
||||||
|
node["hw_model"] = new JSONValue(tempNodeInfo->user.hw_model);
|
||||||
|
|
||||||
nodesArray.push_back(new JSONValue(node));
|
nodesArray.push_back(new JSONValue(node));
|
||||||
}
|
}
|
||||||
@@ -930,4 +930,4 @@ void handleScanNetworks(HTTPRequest *req, HTTPResponse *res)
|
|||||||
res->print(jsonString.c_str());
|
res->print(jsonString.c_str());
|
||||||
delete value;
|
delete value;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -37,43 +37,9 @@
|
|||||||
#define MAX_RX_NOTIFICATION_TOPHONE 2
|
#define MAX_RX_NOTIFICATION_TOPHONE 2
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/// Tighten this when the slim header shrinks; loosen only with deliberate
|
/// Verify baseline assumption of node size. If it increases, we need to reevaluate
|
||||||
/// awareness of MAX_NUM_NODES impact per platform.
|
/// the impact of its memory footprint, notably on MAX_NUM_NODES.
|
||||||
static_assert(sizeof(meshtastic_NodeInfoLite) <= 130, "NodeInfoLite size increased. Reconsider impact on MAX_NUM_NODES.");
|
static_assert(sizeof(meshtastic_NodeInfoLite) <= 200, "NodeInfoLite size increased. Reconsider impact on MAX_NUM_NODES.");
|
||||||
|
|
||||||
// Compile satellite NodeDBs out on STM32WL (and the status DB also follows
|
|
||||||
// MESHTASTIC_EXCLUDE_STATUS).
|
|
||||||
#ifndef MESHTASTIC_EXCLUDE_POSITIONDB
|
|
||||||
#if defined(ARCH_STM32WL)
|
|
||||||
#define MESHTASTIC_EXCLUDE_POSITIONDB 1
|
|
||||||
#else
|
|
||||||
#define MESHTASTIC_EXCLUDE_POSITIONDB 0
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef MESHTASTIC_EXCLUDE_TELEMETRYDB
|
|
||||||
#if defined(ARCH_STM32WL)
|
|
||||||
#define MESHTASTIC_EXCLUDE_TELEMETRYDB 1
|
|
||||||
#else
|
|
||||||
#define MESHTASTIC_EXCLUDE_TELEMETRYDB 0
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
|
||||||
#if defined(ARCH_STM32WL)
|
|
||||||
#define MESHTASTIC_EXCLUDE_ENVIRONMENTDB 1
|
|
||||||
#else
|
|
||||||
#define MESHTASTIC_EXCLUDE_ENVIRONMENTDB 0
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef MESHTASTIC_EXCLUDE_STATUSDB
|
|
||||||
#if defined(ARCH_STM32WL) || defined(MESHTASTIC_EXCLUDE_STATUS)
|
|
||||||
#define MESHTASTIC_EXCLUDE_STATUSDB 1
|
|
||||||
#else
|
|
||||||
#define MESHTASTIC_EXCLUDE_STATUSDB 0
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/// max number of nodes allowed in the nodeDB
|
/// max number of nodes allowed in the nodeDB
|
||||||
#ifndef MAX_NUM_NODES
|
#ifndef MAX_NUM_NODES
|
||||||
|
|||||||
@@ -1,34 +1,22 @@
|
|||||||
/*
|
/*
|
||||||
Adds a WebServer and WebService callbacks to meshtastic via the Portduino/native target (Linux and
|
Adds a WebServer and WebService callbacks to meshtastic as Linux Version. The WebServer & Webservices
|
||||||
macOS). The WebServer & Webservices run in a real host thread beside the Portduino threading
|
runs in a real linux thread beside the portdunio threading emulation. It replaces the complete ESP32
|
||||||
emulation. It replaces the complete ESP32 Webserver libs including generation of SSL certificates,
|
Webserver libs including generation of SSL certifcicates, because the use ESP specific details in
|
||||||
because those libs use ESP-specific details that can't be emulated.
|
the lib that can't be emulated.
|
||||||
|
|
||||||
The WebServices adapt to the two major phoneapi functions "handleAPIv1FromRadio,handleAPIv1ToRadio"
|
The WebServices adapt to the two major phoneapi functions "handleAPIv1FromRadio,handleAPIv1ToRadio"
|
||||||
The WebServer just adds basic support to deliver WebContent, so it can be used to
|
The WebServer just adds basaic support to deliver WebContent, so it can be used to
|
||||||
deliver the WebGui defined by the WebClient Project.
|
deliver the WebGui definded by the WebClient Project.
|
||||||
|
|
||||||
Steps to get it running:
|
Steps to get it running:
|
||||||
|
1.) Add these Linux Libs to the compile and target machine:
|
||||||
Linux (apt):
|
|
||||||
1.) Add these libs to the compile and target machine:
|
|
||||||
|
|
||||||
sudo apt update && \
|
sudo apt update && \
|
||||||
apt -y install openssl libssl-dev libsdl2-dev \
|
apt -y install openssl libssl-dev libopenssl libsdl2-dev \
|
||||||
libulfius-dev liborcania-dev
|
libulfius-dev liborcania-dev
|
||||||
|
|
||||||
macOS (Homebrew):
|
|
||||||
1.) Install prerequisites via Homebrew:
|
|
||||||
|
|
||||||
brew install ulfius openssl@3
|
|
||||||
|
|
||||||
The PlatformIO env (native-macos) picks up compiler/linker flags via
|
|
||||||
`pkg-config`. In particular, OpenSSL needs `pkg-config --cflags --libs openssl@3`
|
|
||||||
so both the Homebrew include path and linker flags are provided; ulfius and its
|
|
||||||
dependencies (liborcania, libyder) are also resolved via `pkg-config`.
|
|
||||||
|
|
||||||
2.) Configure the root directory of the web Content in the config.yaml file.
|
2.) Configure the root directory of the web Content in the config.yaml file.
|
||||||
The following tags should be included and set at your needs
|
The followinng tags should be included and set at your needs
|
||||||
|
|
||||||
Example entry in the config.yaml
|
Example entry in the config.yaml
|
||||||
Webserver:
|
Webserver:
|
||||||
@@ -46,10 +34,7 @@ Author: Marc Philipp Hammermann
|
|||||||
mail: marchammermann@googlemail.com
|
mail: marchammermann@googlemail.com
|
||||||
|
|
||||||
*/
|
*/
|
||||||
// Mirrors the guard in PiWebServer.h — see comment there. macOS Homebrew
|
#ifdef PORTDUINO_LINUX_HARDWARE
|
||||||
// provides ulfius + deps; Linux pulls them via apt. Either way, this
|
|
||||||
// translation unit only compiles when the headers are present.
|
|
||||||
#ifdef ARCH_PORTDUINO
|
|
||||||
#if __has_include(<ulfius.h>)
|
#if __has_include(<ulfius.h>)
|
||||||
#include "PiWebServer.h"
|
#include "PiWebServer.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// Portduino webserver is built whenever the ulfius headers are reachable,
|
#ifdef PORTDUINO_LINUX_HARDWARE
|
||||||
// not only on Linux. macOS users can `brew install ulfius` to enable it;
|
|
||||||
// without ulfius the entire body is skipped and main.cpp's matching
|
|
||||||
// __has_include guard avoids referencing the type.
|
|
||||||
#ifdef ARCH_PORTDUINO
|
|
||||||
#if __has_include(<ulfius.h>)
|
#if __has_include(<ulfius.h>)
|
||||||
#include "PhoneAPI.h"
|
#include "PhoneAPI.h"
|
||||||
#include "ulfius-cfg.h"
|
#include "ulfius-cfg.h"
|
||||||
|
|||||||
+14
-17
@@ -107,14 +107,14 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
|||||||
|
|
||||||
// Automatically favorite the node that is using the admin key
|
// Automatically favorite the node that is using the admin key
|
||||||
auto remoteNode = nodeDB->getMeshNode(mp.from);
|
auto remoteNode = nodeDB->getMeshNode(mp.from);
|
||||||
if (remoteNode && !nodeInfoLiteIsFavorite(remoteNode)) {
|
if (remoteNode && !remoteNode->is_favorite) {
|
||||||
if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) {
|
if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) {
|
||||||
// Special case for CLIENT_BASE: is_favorite has special meaning, and we don't want to automatically set it
|
// Special case for CLIENT_BASE: is_favorite has special meaning, and we don't want to automatically set it
|
||||||
// without the user doing so deliberately.
|
// without the user doing so deliberately.
|
||||||
LOG_INFO("PKC admin valid, but not auto-favoriting node %x because role==CLIENT_BASE", mp.from);
|
LOG_INFO("PKC admin valid, but not auto-favoriting node %x because role==CLIENT_BASE", mp.from);
|
||||||
} else {
|
} else {
|
||||||
LOG_INFO("PKC admin valid. Auto-favoriting node %x", mp.from);
|
LOG_INFO("PKC admin valid. Auto-favoriting node %x", mp.from);
|
||||||
nodeInfoLiteSetBit(remoteNode, NODEINFO_BITFIELD_IS_FAVORITE_MASK, true);
|
remoteNode->is_favorite = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -393,7 +393,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
|||||||
LOG_INFO("Client received set_favorite_node command");
|
LOG_INFO("Client received set_favorite_node command");
|
||||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_favorite_node);
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_favorite_node);
|
||||||
if (node != NULL) {
|
if (node != NULL) {
|
||||||
nodeInfoLiteSetBit(node, NODEINFO_BITFIELD_IS_FAVORITE_MASK, true);
|
node->is_favorite = true;
|
||||||
saveChanges(SEGMENT_NODEDATABASE, false);
|
saveChanges(SEGMENT_NODEDATABASE, false);
|
||||||
if (screen)
|
if (screen)
|
||||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens
|
screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens
|
||||||
@@ -404,7 +404,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
|||||||
LOG_INFO("Client received remove_favorite_node command");
|
LOG_INFO("Client received remove_favorite_node command");
|
||||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_favorite_node);
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_favorite_node);
|
||||||
if (node != NULL) {
|
if (node != NULL) {
|
||||||
nodeInfoLiteSetBit(node, NODEINFO_BITFIELD_IS_FAVORITE_MASK, false);
|
node->is_favorite = false;
|
||||||
saveChanges(SEGMENT_NODEDATABASE, false);
|
saveChanges(SEGMENT_NODEDATABASE, false);
|
||||||
if (screen)
|
if (screen)
|
||||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens
|
screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens
|
||||||
@@ -415,10 +415,11 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
|||||||
LOG_INFO("Client received set_ignored_node command");
|
LOG_INFO("Client received set_ignored_node command");
|
||||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_ignored_node);
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_ignored_node);
|
||||||
if (node != NULL) {
|
if (node != NULL) {
|
||||||
nodeInfoLiteSetBit(node, NODEINFO_BITFIELD_IS_IGNORED_MASK, true);
|
node->is_ignored = true;
|
||||||
nodeDB->eraseNodeSatellites(node->num);
|
node->has_device_metrics = false;
|
||||||
node->public_key.size = 0;
|
node->has_position = false;
|
||||||
memset(node->public_key.bytes, 0, sizeof(node->public_key.bytes));
|
node->user.public_key.size = 0;
|
||||||
|
memset(node->user.public_key.bytes, 0, sizeof(node->user.public_key.bytes));
|
||||||
saveChanges(SEGMENT_NODEDATABASE, false);
|
saveChanges(SEGMENT_NODEDATABASE, false);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -427,7 +428,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
|||||||
LOG_INFO("Client received remove_ignored_node command");
|
LOG_INFO("Client received remove_ignored_node command");
|
||||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_ignored_node);
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_ignored_node);
|
||||||
if (node != NULL) {
|
if (node != NULL) {
|
||||||
nodeInfoLiteSetBit(node, NODEINFO_BITFIELD_IS_IGNORED_MASK, false);
|
node->is_ignored = false;
|
||||||
saveChanges(SEGMENT_NODEDATABASE, false);
|
saveChanges(SEGMENT_NODEDATABASE, false);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -436,7 +437,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
|||||||
LOG_INFO("Client received toggle_muted_node command");
|
LOG_INFO("Client received toggle_muted_node command");
|
||||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->toggle_muted_node);
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->toggle_muted_node);
|
||||||
if (node != NULL) {
|
if (node != NULL) {
|
||||||
nodeInfoLiteSetBit(node, NODEINFO_BITFIELD_IS_MUTED_MASK, !nodeInfoLiteIsMuted(node));
|
node->bitfield ^= (1 << NODEINFO_BITFIELD_IS_MUTED_SHIFT);
|
||||||
saveChanges(SEGMENT_NODEDATABASE, false);
|
saveChanges(SEGMENT_NODEDATABASE, false);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -444,11 +445,9 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
|||||||
|
|
||||||
case meshtastic_AdminMessage_set_fixed_position_tag: {
|
case meshtastic_AdminMessage_set_fixed_position_tag: {
|
||||||
LOG_INFO("Client received set_fixed_position command");
|
LOG_INFO("Client received set_fixed_position command");
|
||||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||||
// Route the fixed position through updatePosition so it lands in the
|
node->has_position = true;
|
||||||
// satellite map (or, on builds with PositionDB excluded, just sets
|
node->position = TypeConversions::ConvertToPositionLite(r->set_fixed_position);
|
||||||
// localPosition for the local broadcast path).
|
|
||||||
nodeDB->updatePosition(node->num, r->set_fixed_position, RX_SRC_LOCAL);
|
|
||||||
nodeDB->setLocalPosition(r->set_fixed_position);
|
nodeDB->setLocalPosition(r->set_fixed_position);
|
||||||
config.position.fixed_position = true;
|
config.position.fixed_position = true;
|
||||||
saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false);
|
saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false);
|
||||||
@@ -1413,9 +1412,7 @@ void AdminModule::saveChanges(int saveWhat, bool shouldReboot)
|
|||||||
|
|
||||||
void AdminModule::handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uicfg)
|
void AdminModule::handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uicfg)
|
||||||
{
|
{
|
||||||
#if HAS_SCREEN
|
|
||||||
nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg, &uicfg);
|
nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg, &uicfg);
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void AdminModule::handleSetHamMode(const meshtastic_HamParameters &p)
|
void AdminModule::handleSetHamMode(const meshtastic_HamParameters &p)
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ void CannedMessageModule::LaunchFreetextWithDestination(NodeNum newDest, uint8_t
|
|||||||
static bool returnToCannedList = false;
|
static bool returnToCannedList = false;
|
||||||
bool hasKeyForNode(const meshtastic_NodeInfoLite *node)
|
bool hasKeyForNode(const meshtastic_NodeInfoLite *node)
|
||||||
{
|
{
|
||||||
return nodeInfoLiteHasUser(node) && node->public_key.size > 0;
|
return node && node->has_user && node->user.public_key.size > 0;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @brief Items in array this->messages will be set to be pointing on the right
|
* @brief Items in array this->messages will be set to be pointing on the right
|
||||||
@@ -262,10 +262,10 @@ void CannedMessageModule::updateDestinationSelectionList()
|
|||||||
|
|
||||||
for (size_t i = 0; i < numMeshNodes; ++i) {
|
for (size_t i = 0; i < numMeshNodes; ++i) {
|
||||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||||
if (!node || node->num == myNodeNum || !nodeInfoLiteHasUser(node) || node->public_key.size != 32)
|
if (!node || node->num == myNodeNum || !node->has_user || node->user.public_key.size != 32)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
const String &nodeName = node->long_name;
|
const String &nodeName = node->user.long_name;
|
||||||
|
|
||||||
if (searchQuery.length() == 0) {
|
if (searchQuery.length() == 0) {
|
||||||
this->filteredNodes.push_back({node, sinceLastSeen(node)});
|
this->filteredNodes.push_back({node, sinceLastSeen(node)});
|
||||||
@@ -1054,7 +1054,7 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha
|
|||||||
}
|
}
|
||||||
|
|
||||||
NodeNum myNodeNum = nodeDB->getNodeNum();
|
NodeNum myNodeNum = nodeDB->getNodeNum();
|
||||||
if (node && node->num != myNodeNum && nodeInfoLiteHasUser(node) && node->public_key.size == 32) {
|
if (node && node->num != myNodeNum && node->has_user && node->user.public_key.size == 32) {
|
||||||
p->pki_encrypted = true;
|
p->pki_encrypted = true;
|
||||||
p->channel = 0; // force PKI
|
p->channel = 0; // force PKI
|
||||||
}
|
}
|
||||||
@@ -1415,8 +1415,8 @@ const char *CannedMessageModule::getNodeName(NodeNum node)
|
|||||||
return "Broadcast";
|
return "Broadcast";
|
||||||
|
|
||||||
meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node);
|
meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node);
|
||||||
if (nodeInfoLiteHasUser(info) && strlen(info->long_name) > 0) {
|
if (info && info->has_user && strlen(info->user.long_name) > 0) {
|
||||||
return info->long_name;
|
return info->user.long_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
static char fallback[12];
|
static char fallback[12];
|
||||||
@@ -1723,17 +1723,17 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O
|
|||||||
meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node;
|
meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node;
|
||||||
if (node) {
|
if (node) {
|
||||||
if (display->getWidth() <= 64) {
|
if (display->getWidth() <= 64) {
|
||||||
entryText = node->short_name;
|
entryText = node->user.short_name;
|
||||||
} else if (node->long_name[0]) {
|
} else if (node->user.long_name[0]) {
|
||||||
entryText = node->long_name;
|
entryText = node->user.long_name;
|
||||||
} else {
|
} else {
|
||||||
entryText = node->short_name;
|
entryText = node->user.short_name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int availWidth = display->getWidth() -
|
int availWidth = display->getWidth() -
|
||||||
((graphics::currentResolution == graphics::ScreenResolution::High) ? 40 : 20) -
|
((graphics::currentResolution == graphics::ScreenResolution::High) ? 40 : 20) -
|
||||||
((nodeInfoLiteIsFavorite(node)) ? 10 : 0);
|
((node && node->is_favorite) ? 10 : 0);
|
||||||
if (availWidth < 0)
|
if (availWidth < 0)
|
||||||
availWidth = 0;
|
availWidth = 0;
|
||||||
char truncatedEntry[96];
|
char truncatedEntry[96];
|
||||||
@@ -1742,7 +1742,7 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O
|
|||||||
entryText = truncatedEntry;
|
entryText = truncatedEntry;
|
||||||
|
|
||||||
// Prepend "* " if this is a favorite
|
// Prepend "* " if this is a favorite
|
||||||
if (nodeInfoLiteIsFavorite(node)) {
|
if (node && node->is_favorite) {
|
||||||
entryText = "* " + entryText;
|
entryText = "* " + entryText;
|
||||||
}
|
}
|
||||||
graphics::UIRenderer::truncateStringWithEmotes(display, entryText.c_str(), truncatedEntry, sizeof(truncatedEntry),
|
graphics::UIRenderer::truncateStringWithEmotes(display, entryText.c_str(), truncatedEntry, sizeof(truncatedEntry),
|
||||||
|
|||||||
@@ -0,0 +1,604 @@
|
|||||||
|
#include "DMShell.h"
|
||||||
|
|
||||||
|
#if defined(ARCH_PORTDUINO)
|
||||||
|
|
||||||
|
#include "Channels.h"
|
||||||
|
#include "MeshService.h"
|
||||||
|
#include "NodeDB.h"
|
||||||
|
#include "Throttle.h"
|
||||||
|
#include "configuration.h"
|
||||||
|
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||||
|
#include "mesh/mesh-pb-constants.h"
|
||||||
|
#include "pb_decode.h"
|
||||||
|
#include "pb_encode.h"
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <pty.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/ioctl.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
DMShellModule *dmShellModule;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr uint16_t PTY_COLS_DEFAULT = 120;
|
||||||
|
constexpr uint16_t PTY_ROWS_DEFAULT = 40;
|
||||||
|
constexpr size_t MAX_MESSAGE_SIZE = 200;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
DMShellModule::DMShellModule()
|
||||||
|
: SinglePortModule("DMShellModule", meshtastic_PortNum_REMOTE_SHELL_APP), concurrency::OSThread("DMShell", 100)
|
||||||
|
{
|
||||||
|
LOG_WARN("DMShell enabled on Portduino: remote shell access is dangerous and intended for trusted debugging only");
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessMessage DMShellModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||||
|
{
|
||||||
|
meshtastic_RemoteShell frame = meshtastic_RemoteShell_init_zero;
|
||||||
|
if (!mp.pki_encrypted) {
|
||||||
|
LOG_WARN("DMShell: ignoring packet without PKI from 0x%x", mp.from);
|
||||||
|
return ProcessMessage::STOP;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parseFrame(mp, frame)) {
|
||||||
|
LOG_WARN("DMShell: ignoring malformed frame");
|
||||||
|
return ProcessMessage::STOP;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame.op == meshtastic_RemoteShell_OpCode_ACK) {
|
||||||
|
if (session.active && frame.session_id == session.sessionId && getFrom(&mp) == session.peer && frame.last_rx_seq > 0) {
|
||||||
|
resendFramesFrom(frame.last_rx_seq + 1);
|
||||||
|
}
|
||||||
|
return ProcessMessage::CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame.op >= 64) {
|
||||||
|
LOG_WARN("DMShell: ignoring frame with op code %d, seq %d", frame.op, frame.seq);
|
||||||
|
return ProcessMessage::CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAuthorizedPacket(mp)) {
|
||||||
|
LOG_WARN("DMShell: unauthorized sender 0x%x, %u", mp.from, frame.op);
|
||||||
|
myReply = allocErrorResponse(meshtastic_Routing_Error_NOT_AUTHORIZED, &mp);
|
||||||
|
return ProcessMessage::STOP;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame.op == meshtastic_RemoteShell_OpCode_OPEN) {
|
||||||
|
LOG_WARN("DMShell: received OPEN from 0x%x sessionId=0x%x", mp.from, frame.session_id);
|
||||||
|
if (!openSession(mp, frame)) {
|
||||||
|
sendError("open_failed", getFrom(&mp));
|
||||||
|
}
|
||||||
|
return ProcessMessage::STOP;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session.active || frame.session_id != session.sessionId || getFrom(&mp) != session.peer) {
|
||||||
|
if (!session.active) {
|
||||||
|
LOG_WARN("DMShell: no active session, rejecting op %d from 0x%x", frame.op, mp.from);
|
||||||
|
} else {
|
||||||
|
LOG_WARN("DMShell: session ID mismatch (got 0x%x expected 0x%x) or peer mismatch (got 0x%x expected 0x%x), rejecting "
|
||||||
|
"op %d",
|
||||||
|
frame.session_id, session.sessionId, mp.from, session.peer, frame.op);
|
||||||
|
}
|
||||||
|
sendError("invalid_session", getFrom(&mp));
|
||||||
|
return ProcessMessage::STOP;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldProcessIncomingFrame(frame)) {
|
||||||
|
return ProcessMessage::STOP;
|
||||||
|
}
|
||||||
|
|
||||||
|
session.lastActivityMs = millis();
|
||||||
|
|
||||||
|
switch (frame.op) {
|
||||||
|
case meshtastic_RemoteShell_OpCode_INPUT:
|
||||||
|
if (!writeSessionInput(frame)) {
|
||||||
|
sendError("input_write_failed");
|
||||||
|
} else {
|
||||||
|
uint8_t outBuf[MAX_MESSAGE_SIZE];
|
||||||
|
const ssize_t bytesRead = read(session.masterFd, outBuf, sizeof(outBuf));
|
||||||
|
if (bytesRead > 0) {
|
||||||
|
LOG_WARN("DMShell: read %zd bytes from PTY", bytesRead);
|
||||||
|
meshtastic_RemoteShell frame = {
|
||||||
|
.op = meshtastic_RemoteShell_OpCode_OUTPUT,
|
||||||
|
.session_id = session.sessionId,
|
||||||
|
.seq = session.nextTxSeq++,
|
||||||
|
.ack_seq = session.lastAckedRxSeq,
|
||||||
|
.cols = 0,
|
||||||
|
.rows = 0,
|
||||||
|
.flags = 0,
|
||||||
|
};
|
||||||
|
assert(bytesRead <= sizeof(frame.payload.bytes));
|
||||||
|
memcpy(frame.payload.bytes, outBuf, bytesRead);
|
||||||
|
frame.payload.size = bytesRead;
|
||||||
|
sendFrameToPeer(session.peer, frame, true);
|
||||||
|
session.lastActivityMs = millis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case meshtastic_RemoteShell_OpCode_RESIZE:
|
||||||
|
if (frame.rows > 0 && frame.cols > 0) {
|
||||||
|
struct winsize ws = {};
|
||||||
|
ws.ws_row = frame.rows;
|
||||||
|
ws.ws_col = frame.cols;
|
||||||
|
if (session.masterFd >= 0) {
|
||||||
|
ioctl(session.masterFd, TIOCSWINSZ, &ws);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case meshtastic_RemoteShell_OpCode_PING: {
|
||||||
|
uint32_t peerLastRxSeq = frame.ack_seq;
|
||||||
|
if (frame.last_rx_seq > 0) {
|
||||||
|
peerLastRxSeq = frame.last_rx_seq;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t nextMissingForPeer = peerLastRxSeq + 1;
|
||||||
|
if (nextMissingForPeer > 0 && nextMissingForPeer < session.nextTxSeq) {
|
||||||
|
resendFramesFrom(nextMissingForPeer);
|
||||||
|
}
|
||||||
|
|
||||||
|
meshtastic_RemoteShell frame = {
|
||||||
|
.op = meshtastic_RemoteShell_OpCode_PONG,
|
||||||
|
.session_id = session.sessionId,
|
||||||
|
.seq = session.nextTxSeq++,
|
||||||
|
.ack_seq = session.lastAckedRxSeq,
|
||||||
|
.cols = 0,
|
||||||
|
.rows = 0,
|
||||||
|
.flags = 0,
|
||||||
|
.last_tx_seq = session.nextTxSeq > 0 ? session.nextTxSeq - 1 : 0,
|
||||||
|
.last_rx_seq = session.lastAckedRxSeq,
|
||||||
|
};
|
||||||
|
frame.payload.size = 0;
|
||||||
|
sendFrameToPeer(session.peer, frame, true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case meshtastic_RemoteShell_OpCode_CLOSE:
|
||||||
|
closeSession("peer_close", true);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
sendError("unsupported_op");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProcessMessage::STOP;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t DMShellModule::runOnce()
|
||||||
|
{
|
||||||
|
processPendingChildReap();
|
||||||
|
|
||||||
|
if (!session.active) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
reapChildIfExited();
|
||||||
|
if (!session.active) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Throttle::isWithinTimespanMs(session.lastActivityMs, SESSION_IDLE_TIMEOUT_MS) == false) {
|
||||||
|
closeSession("idle_timeout", true);
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RadioLibInterface::instance->packetsInTxQueue() > 1) {
|
||||||
|
return 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t outBuf[MAX_MESSAGE_SIZE];
|
||||||
|
while (session.masterFd >= 0) {
|
||||||
|
const ssize_t bytesRead = read(session.masterFd, outBuf, sizeof(outBuf));
|
||||||
|
if (bytesRead > 0) {
|
||||||
|
LOG_WARN("DMShell: read %zd bytes from PTY", bytesRead);
|
||||||
|
|
||||||
|
meshtastic_RemoteShell frame = {
|
||||||
|
.op = meshtastic_RemoteShell_OpCode_OUTPUT,
|
||||||
|
.session_id = session.sessionId,
|
||||||
|
.seq = session.nextTxSeq++,
|
||||||
|
.ack_seq = session.lastAckedRxSeq,
|
||||||
|
.cols = 0,
|
||||||
|
.rows = 0,
|
||||||
|
.flags = 0,
|
||||||
|
};
|
||||||
|
assert(bytesRead <= sizeof(frame.payload.bytes));
|
||||||
|
memcpy(frame.payload.bytes, outBuf, bytesRead);
|
||||||
|
frame.payload.size = bytesRead;
|
||||||
|
sendFrameToPeer(session.peer, frame, true);
|
||||||
|
|
||||||
|
session.lastActivityMs = millis();
|
||||||
|
// continue;
|
||||||
|
// do we want to ack every data message, and only send the next on ack?
|
||||||
|
// would require some retry logic. Maybe re-use the wantAck bit
|
||||||
|
return 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytesRead == 0) {
|
||||||
|
closeSession("pty_eof", true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_WARN("DMShell: PTY read error errno=%d", errno);
|
||||||
|
closeSession("pty_read_error", true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DMShellModule::parseFrame(const meshtastic_MeshPacket &mp, meshtastic_RemoteShell &outFrame)
|
||||||
|
{
|
||||||
|
if (mp.which_payload_variant != meshtastic_MeshPacket_decoded_tag) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_RemoteShell_fields, &outFrame)) {
|
||||||
|
LOG_INFO("Received a DMShell message");
|
||||||
|
} else {
|
||||||
|
LOG_ERROR("Error decoding DMShell message!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DMShellModule::isAuthorizedPacket(const meshtastic_MeshPacket &mp) const
|
||||||
|
{
|
||||||
|
if (mp.from == 0) {
|
||||||
|
return !config.security.is_managed;
|
||||||
|
}
|
||||||
|
|
||||||
|
const meshtastic_Channel *ch = &channels.getByIndex(mp.channel);
|
||||||
|
if (strcasecmp(ch->settings.name, Channels::adminChannel) == 0) {
|
||||||
|
return config.security.admin_channel_enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mp.pki_encrypted) {
|
||||||
|
for (uint8_t i = 0; i < 3; ++i) {
|
||||||
|
if (config.security.admin_key[i].size == 32 &&
|
||||||
|
memcmp(mp.public_key.bytes, config.security.admin_key[i].bytes, 32) == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DMShellModule::openSession(const meshtastic_MeshPacket &mp, const meshtastic_RemoteShell &frame)
|
||||||
|
{
|
||||||
|
if (session.active) {
|
||||||
|
closeSession("preempted", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
int masterFd = -1;
|
||||||
|
struct winsize ws = {};
|
||||||
|
if (frame.rows > 0) {
|
||||||
|
ws.ws_row = frame.rows;
|
||||||
|
} else {
|
||||||
|
ws.ws_row = PTY_ROWS_DEFAULT;
|
||||||
|
}
|
||||||
|
if (frame.cols > 0) {
|
||||||
|
ws.ws_col = frame.cols;
|
||||||
|
} else {
|
||||||
|
ws.ws_col = PTY_COLS_DEFAULT;
|
||||||
|
}
|
||||||
|
const pid_t childPid = forkpty(&masterFd, nullptr, nullptr, &ws);
|
||||||
|
if (childPid < 0) {
|
||||||
|
LOG_ERROR("DMShell: forkpty failed errno=%d", errno);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (childPid == 0) {
|
||||||
|
const char *shell = getenv("SHELL");
|
||||||
|
if (!shell || !*shell) {
|
||||||
|
shell = "/bin/sh";
|
||||||
|
}
|
||||||
|
execl(shell, shell, "-i", static_cast<char *>(nullptr));
|
||||||
|
_exit(127);
|
||||||
|
}
|
||||||
|
|
||||||
|
const int flags = fcntl(masterFd, F_GETFL, 0);
|
||||||
|
if (flags >= 0) {
|
||||||
|
fcntl(masterFd, F_SETFL, flags | O_NONBLOCK);
|
||||||
|
}
|
||||||
|
|
||||||
|
session.active = true;
|
||||||
|
session.sessionId = (frame.session_id != 0) ? frame.session_id : static_cast<uint32_t>(random(1, 0x7fffffff));
|
||||||
|
session.peer = getFrom(&mp);
|
||||||
|
session.channel = mp.channel;
|
||||||
|
session.masterFd = masterFd;
|
||||||
|
session.childPid = childPid;
|
||||||
|
session.nextTxSeq = 1;
|
||||||
|
session.lastAckedRxSeq = frame.seq;
|
||||||
|
session.nextExpectedRxSeq = frame.seq + 1;
|
||||||
|
session.highestSeenRxSeq = frame.seq;
|
||||||
|
session.lastActivityMs = millis();
|
||||||
|
|
||||||
|
meshtastic_RemoteShell newFrame = {
|
||||||
|
.op = meshtastic_RemoteShell_OpCode_OPEN_OK,
|
||||||
|
.session_id = session.sessionId,
|
||||||
|
.seq = session.nextTxSeq++,
|
||||||
|
.ack_seq = frame.seq,
|
||||||
|
.cols = ws.ws_col,
|
||||||
|
.rows = ws.ws_row,
|
||||||
|
.flags = 0,
|
||||||
|
};
|
||||||
|
newFrame.payload.size = 0;
|
||||||
|
sendFrameToPeer(session.peer, newFrame, true);
|
||||||
|
|
||||||
|
LOG_INFO("DMShell: opened session=0x%x peer=0x%x pid=%d", session.sessionId, session.peer, session.childPid);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DMShellModule::writeSessionInput(const meshtastic_RemoteShell &frame)
|
||||||
|
{
|
||||||
|
if (session.masterFd < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (frame.payload.size == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ssize_t bytesWritten = write(session.masterFd, frame.payload.bytes, frame.payload.size);
|
||||||
|
return bytesWritten >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DMShellModule::closeSession(const char *reason, bool notifyPeer)
|
||||||
|
{
|
||||||
|
if (!session.active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notifyPeer) {
|
||||||
|
const size_t reasonLen = strnlen(reason, 256);
|
||||||
|
meshtastic_RemoteShell frame = {
|
||||||
|
.op = meshtastic_RemoteShell_OpCode_CLOSED,
|
||||||
|
.session_id = session.sessionId,
|
||||||
|
.seq = session.nextTxSeq++,
|
||||||
|
.ack_seq = session.lastAckedRxSeq,
|
||||||
|
.cols = 0,
|
||||||
|
.rows = 0,
|
||||||
|
.flags = 0,
|
||||||
|
};
|
||||||
|
assert(reasonLen <= sizeof(frame.payload.bytes));
|
||||||
|
memcpy(frame.payload.bytes, reason, reasonLen);
|
||||||
|
frame.payload.size = reasonLen;
|
||||||
|
sendFrameToPeer(session.peer, frame, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.masterFd >= 0) {
|
||||||
|
close(session.masterFd);
|
||||||
|
session.masterFd = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.childPid > 0) {
|
||||||
|
// Run this to avoid forgetting a child
|
||||||
|
processPendingChildReap();
|
||||||
|
|
||||||
|
if (kill(session.childPid, SIGTERM) < 0 && errno != ESRCH) {
|
||||||
|
LOG_WARN("DMShell: failed to send SIGTERM to pid=%d errno=%d", session.childPid, errno);
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingChildPid = session.childPid;
|
||||||
|
session.childPid = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("DMShell: closed session=0x%x reason=%s", session.sessionId, reason);
|
||||||
|
session = DMShellSession{};
|
||||||
|
}
|
||||||
|
|
||||||
|
void DMShellModule::reapChildIfExited()
|
||||||
|
{
|
||||||
|
if (!session.active || session.childPid <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int status = 0;
|
||||||
|
const pid_t result = waitpid(session.childPid, &status, WNOHANG);
|
||||||
|
if (result == session.childPid) {
|
||||||
|
closeSession("shell_exited", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DMShellModule::processPendingChildReap()
|
||||||
|
{
|
||||||
|
if (pendingChildPid <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int status = 0;
|
||||||
|
const pid_t result = waitpid(pendingChildPid, &status, WNOHANG);
|
||||||
|
|
||||||
|
if (result == pendingChildPid || (result < 0 && errno == ECHILD)) {
|
||||||
|
pendingChildPid = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result < 0) {
|
||||||
|
LOG_WARN("DMShell: waitpid failed for pid=%d errno=%d", pendingChildPid, errno);
|
||||||
|
pendingChildPid = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingChildPid > 0) {
|
||||||
|
if (kill(pendingChildPid, SIGKILL) < 0 && errno != ESRCH) {
|
||||||
|
LOG_WARN("DMShell: failed to send SIGKILL to pid=%d errno=%d", pendingChildPid, errno);
|
||||||
|
}
|
||||||
|
pendingChildPid = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DMShellModule::rememberSentFrame(meshtastic_RemoteShell frame)
|
||||||
|
{
|
||||||
|
if (frame.seq == 0 || frame.op == meshtastic_RemoteShell_OpCode_ACK) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto &entry = session.txHistory[session.txHistoryNext];
|
||||||
|
entry.valid = true;
|
||||||
|
entry.op = frame.op;
|
||||||
|
entry.sessionId = frame.session_id;
|
||||||
|
entry.seq = frame.seq;
|
||||||
|
entry.ackSeq = frame.ack_seq;
|
||||||
|
entry.cols = frame.cols;
|
||||||
|
entry.rows = frame.rows;
|
||||||
|
entry.flags = frame.flags;
|
||||||
|
entry.payloadLen = frame.payload.size;
|
||||||
|
if (frame.payload.size > 0) {
|
||||||
|
memcpy(entry.payload, frame.payload.bytes, frame.payload.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
session.txHistoryNext = (session.txHistoryNext + 1) % session.txHistory.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DMShellModule::resendFramesFrom(uint32_t startSeq)
|
||||||
|
{
|
||||||
|
if (startSeq == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DMShellSession::SentFrame *match = nullptr;
|
||||||
|
for (auto &entry : session.txHistory) {
|
||||||
|
if (!entry.valid || entry.seq != startSeq) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match = &entry;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
LOG_WARN("DMShell: replay request for seq=%u not found in history", startSeq);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("DMShell: replaying frame seq=%u op=%d", match->seq, match->op);
|
||||||
|
meshtastic_RemoteShell frame = {
|
||||||
|
.op = match->op,
|
||||||
|
.session_id = match->sessionId,
|
||||||
|
.seq = match->seq,
|
||||||
|
.ack_seq = match->ackSeq,
|
||||||
|
.cols = match->cols,
|
||||||
|
.rows = match->rows,
|
||||||
|
.flags = match->flags,
|
||||||
|
};
|
||||||
|
assert(match->payloadLen <= sizeof(frame.payload.bytes));
|
||||||
|
memcpy(frame.payload.bytes, match->payload, match->payloadLen);
|
||||||
|
frame.payload.size = match->payloadLen;
|
||||||
|
sendFrameToPeer(session.peer, frame, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DMShellModule::sendAck(uint32_t replayFromSeq)
|
||||||
|
{
|
||||||
|
if (replayFromSeq > 0) {
|
||||||
|
LOG_WARN("DMShell: requesting replay from seq=%u", replayFromSeq);
|
||||||
|
}
|
||||||
|
meshtastic_RemoteShell frame = {
|
||||||
|
.op = meshtastic_RemoteShell_OpCode_ACK,
|
||||||
|
.session_id = session.sessionId,
|
||||||
|
.seq = 0,
|
||||||
|
.ack_seq = session.lastAckedRxSeq,
|
||||||
|
.cols = 0,
|
||||||
|
.rows = 0,
|
||||||
|
.flags = 0,
|
||||||
|
.last_rx_seq = replayFromSeq - 1,
|
||||||
|
};
|
||||||
|
frame.payload.size = 0;
|
||||||
|
sendFrameToPeer(session.peer, frame, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DMShellModule::shouldProcessIncomingFrame(const meshtastic_RemoteShell &frame)
|
||||||
|
{
|
||||||
|
if (frame.seq == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame.seq < session.nextExpectedRxSeq) {
|
||||||
|
if (session.highestSeenRxSeq >= session.nextExpectedRxSeq) {
|
||||||
|
sendAck(session.nextExpectedRxSeq);
|
||||||
|
} else {
|
||||||
|
sendAck();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame.seq > session.nextExpectedRxSeq) {
|
||||||
|
if (frame.seq > session.highestSeenRxSeq) {
|
||||||
|
session.highestSeenRxSeq = frame.seq;
|
||||||
|
}
|
||||||
|
sendAck(session.nextExpectedRxSeq);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
session.lastAckedRxSeq = frame.seq;
|
||||||
|
session.nextExpectedRxSeq = frame.seq + 1;
|
||||||
|
if (frame.seq > session.highestSeenRxSeq) {
|
||||||
|
session.highestSeenRxSeq = frame.seq;
|
||||||
|
}
|
||||||
|
if (session.highestSeenRxSeq >= session.nextExpectedRxSeq) {
|
||||||
|
sendAck(session.nextExpectedRxSeq);
|
||||||
|
} else {
|
||||||
|
session.highestSeenRxSeq = 0;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DMShellModule::sendFrameToPeer(NodeNum peer, meshtastic_RemoteShell frame, bool remember)
|
||||||
|
{
|
||||||
|
meshtastic_MeshPacket *packet = allocDataPacket();
|
||||||
|
if (!packet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LOG_WARN("DMShell: building packet op=%u session=0x%x seq=%u payloadLen=%zu", frame.op, frame.session_id, frame.seq,
|
||||||
|
frame.payload.size);
|
||||||
|
const size_t encoded = pb_encode_to_bytes(packet->decoded.payload.bytes, sizeof(packet->decoded.payload.bytes),
|
||||||
|
meshtastic_RemoteShell_fields, &frame);
|
||||||
|
if (encoded == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
packet->decoded.payload.size = encoded;
|
||||||
|
|
||||||
|
if (remember) {
|
||||||
|
rememberSentFrame(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
packet->to = peer;
|
||||||
|
packet->hop_limit = 0;
|
||||||
|
packet->hop_start = 0;
|
||||||
|
packet->channel = 0;
|
||||||
|
packet->want_ack = false;
|
||||||
|
packet->pki_encrypted = true;
|
||||||
|
packet->priority = meshtastic_MeshPacket_Priority_RELIABLE;
|
||||||
|
service->sendToMesh(packet);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DMShellModule::sendError(const char *message, NodeNum peer)
|
||||||
|
{
|
||||||
|
const size_t len = strnlen(message, MAX_MESSAGE_SIZE);
|
||||||
|
meshtastic_RemoteShell frame = {
|
||||||
|
.op = meshtastic_RemoteShell_OpCode_ERROR,
|
||||||
|
.session_id = session.sessionId,
|
||||||
|
.seq = session.nextTxSeq++,
|
||||||
|
.ack_seq = session.lastAckedRxSeq,
|
||||||
|
.cols = 0,
|
||||||
|
.rows = 0,
|
||||||
|
.flags = 0,
|
||||||
|
};
|
||||||
|
if (message && len > 0) {
|
||||||
|
assert(len <= sizeof(frame.payload.bytes));
|
||||||
|
memcpy(frame.payload.bytes, message, len);
|
||||||
|
frame.payload.size = len;
|
||||||
|
}
|
||||||
|
if (peer == 0) {
|
||||||
|
peer = session.peer;
|
||||||
|
}
|
||||||
|
sendFrameToPeer(peer, frame, true);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "MeshModule.h"
|
||||||
|
#include "Router.h"
|
||||||
|
#include "SinglePortModule.h"
|
||||||
|
#include "concurrency/OSThread.h"
|
||||||
|
#include "configuration.h"
|
||||||
|
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <array>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
#if defined(ARCH_PORTDUINO)
|
||||||
|
|
||||||
|
struct DMShellSession {
|
||||||
|
bool active = false;
|
||||||
|
uint32_t sessionId = 0;
|
||||||
|
NodeNum peer = 0;
|
||||||
|
uint8_t channel = 0;
|
||||||
|
int masterFd = -1;
|
||||||
|
int childPid = -1;
|
||||||
|
uint32_t nextTxSeq = 1;
|
||||||
|
uint32_t lastAckedRxSeq = 0;
|
||||||
|
uint32_t nextExpectedRxSeq = 1;
|
||||||
|
uint32_t highestSeenRxSeq = 0;
|
||||||
|
uint32_t lastActivityMs = 0;
|
||||||
|
struct SentFrame {
|
||||||
|
bool valid = false;
|
||||||
|
meshtastic_RemoteShell_OpCode op = meshtastic_RemoteShell_OpCode_ERROR;
|
||||||
|
uint32_t sessionId = 0;
|
||||||
|
uint32_t seq = 0;
|
||||||
|
uint32_t ackSeq = 0;
|
||||||
|
uint32_t cols = 0;
|
||||||
|
uint32_t rows = 0;
|
||||||
|
uint32_t flags = 0;
|
||||||
|
uint8_t payload[meshtastic_Constants_DATA_PAYLOAD_LEN] = {0};
|
||||||
|
size_t payloadLen = 0;
|
||||||
|
};
|
||||||
|
std::array<SentFrame, 50> txHistory = {};
|
||||||
|
size_t txHistoryNext = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DMShellModule : private concurrency::OSThread, public SinglePortModule
|
||||||
|
{
|
||||||
|
|
||||||
|
public:
|
||||||
|
DMShellModule();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||||
|
virtual int32_t runOnce() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr uint32_t SESSION_IDLE_TIMEOUT_MS = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
DMShellSession session;
|
||||||
|
pid_t pendingChildPid = -1;
|
||||||
|
|
||||||
|
bool parseFrame(const meshtastic_MeshPacket &mp, meshtastic_RemoteShell &outFrame);
|
||||||
|
bool isAuthorizedPacket(const meshtastic_MeshPacket &mp) const;
|
||||||
|
bool openSession(const meshtastic_MeshPacket &mp, const meshtastic_RemoteShell &frame);
|
||||||
|
bool shouldProcessIncomingFrame(const meshtastic_RemoteShell &frame);
|
||||||
|
bool writeSessionInput(const meshtastic_RemoteShell &frame);
|
||||||
|
void closeSession(const char *reason, bool notifyPeer);
|
||||||
|
void reapChildIfExited();
|
||||||
|
void processPendingChildReap();
|
||||||
|
|
||||||
|
void rememberSentFrame(meshtastic_RemoteShell frame);
|
||||||
|
void resendFramesFrom(uint32_t startSeq);
|
||||||
|
void sendAck(uint32_t replayFromSeq = 0);
|
||||||
|
void sendFrameToPeer(NodeNum peer, meshtastic_RemoteShell frame, bool remember = true);
|
||||||
|
void sendError(const char *message, NodeNum peer = 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
extern DMShellModule *dmShellModule;
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -372,7 +372,7 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP
|
|||||||
// If we receive a direct message and the receipent is us, apply DM mute setting
|
// If we receive a direct message and the receipent is us, apply DM mute setting
|
||||||
// Else we just handle it as not muted.
|
// Else we just handle it as not muted.
|
||||||
const bool isDmToUs = !isBroadcast(mp.to) && isToUs(&mp);
|
const bool isDmToUs = !isBroadcast(mp.to) && isToUs(&mp);
|
||||||
bool is_muted = isDmToUs ? nodeInfoLiteIsMuted(sender)
|
bool is_muted = isDmToUs ? (sender && ((sender->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0))
|
||||||
: (ch.settings.has_module_settings && ch.settings.module_settings.is_muted);
|
: (ch.settings.has_module_settings && ch.settings.module_settings.is_muted);
|
||||||
|
|
||||||
const bool buzzerModeIsDirectOnly =
|
const bool buzzerModeIsDirectOnly =
|
||||||
|
|||||||
@@ -10,18 +10,6 @@
|
|||||||
|
|
||||||
KeyVerificationModule *keyVerificationModule;
|
KeyVerificationModule *keyVerificationModule;
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
void copyNodeLongNameOrUnknown(char *dest, size_t destSize, const meshtastic_NodeInfoLite *node)
|
|
||||||
{
|
|
||||||
if (!dest || destSize == 0)
|
|
||||||
return;
|
|
||||||
const char *name = (node && nodeInfoLiteHasUser(node) && node->long_name[0]) ? node->long_name : "Unknown";
|
|
||||||
strncpy(dest, name, destSize - 1);
|
|
||||||
dest[destSize - 1] = '\0';
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
KeyVerificationModule::KeyVerificationModule()
|
KeyVerificationModule::KeyVerificationModule()
|
||||||
: ProtobufModule("KeyVerification", meshtastic_PortNum_KEY_VERIFICATION_APP, &meshtastic_KeyVerification_msg)
|
: ProtobufModule("KeyVerification", meshtastic_PortNum_KEY_VERIFICATION_APP, &meshtastic_KeyVerification_msg)
|
||||||
{
|
{
|
||||||
@@ -49,8 +37,7 @@ AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(cons
|
|||||||
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_VERIFY &&
|
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_VERIFY &&
|
||||||
request->key_verification.nonce == currentNonce) {
|
request->key_verification.nonce == currentNonce) {
|
||||||
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
||||||
if (remoteNodePtr)
|
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||||
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
|
||||||
resetToIdle();
|
resetToIdle();
|
||||||
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY) {
|
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY) {
|
||||||
resetToIdle();
|
resetToIdle();
|
||||||
@@ -85,9 +72,9 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
|
|||||||
sprintf(cn->message, "Enter Security Number for Key Verification");
|
sprintf(cn->message, "Enter Security Number for Key Verification");
|
||||||
cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_request_tag;
|
cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_request_tag;
|
||||||
cn->payload_variant.key_verification_number_request.nonce = currentNonce;
|
cn->payload_variant.key_verification_number_request.nonce = currentNonce;
|
||||||
copyNodeLongNameOrUnknown(cn->payload_variant.key_verification_number_request.remote_longname,
|
strncpy(cn->payload_variant.key_verification_number_request.remote_longname, // should really check for nulls, etc
|
||||||
sizeof(cn->payload_variant.key_verification_number_request.remote_longname),
|
nodeDB->getMeshNode(currentRemoteNode)->user.long_name,
|
||||||
nodeDB->getMeshNode(currentRemoteNode));
|
sizeof(cn->payload_variant.key_verification_number_request.remote_longname));
|
||||||
service->sendClientNotification(cn);
|
service->sendClientNotification(cn);
|
||||||
LOG_INFO("Received hash2");
|
LOG_INFO("Received hash2");
|
||||||
currentState = KEY_VERIFICATION_SENDER_AWAITING_NUMBER;
|
currentState = KEY_VERIFICATION_SENDER_AWAITING_NUMBER;
|
||||||
@@ -108,8 +95,7 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
|
|||||||
[=](int selected) {
|
[=](int selected) {
|
||||||
if (selected == 1) {
|
if (selected == 1) {
|
||||||
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
||||||
if (remoteNodePtr)
|
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||||
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
screen->showOverlayBanner(options);)
|
screen->showOverlayBanner(options);)
|
||||||
@@ -118,9 +104,9 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
|
|||||||
sprintf(cn->message, "Final confirmation for incoming manual key verification %s", message);
|
sprintf(cn->message, "Final confirmation for incoming manual key verification %s", message);
|
||||||
cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag;
|
cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag;
|
||||||
cn->payload_variant.key_verification_final.nonce = currentNonce;
|
cn->payload_variant.key_verification_final.nonce = currentNonce;
|
||||||
copyNodeLongNameOrUnknown(cn->payload_variant.key_verification_final.remote_longname,
|
strncpy(cn->payload_variant.key_verification_final.remote_longname, // should really check for nulls, etc
|
||||||
sizeof(cn->payload_variant.key_verification_final.remote_longname),
|
nodeDB->getMeshNode(currentRemoteNode)->user.long_name,
|
||||||
nodeDB->getMeshNode(currentRemoteNode));
|
sizeof(cn->payload_variant.key_verification_final.remote_longname));
|
||||||
cn->payload_variant.key_verification_final.isSender = false;
|
cn->payload_variant.key_verification_final.isSender = false;
|
||||||
service->sendClientNotification(cn);
|
service->sendClientNotification(cn);
|
||||||
|
|
||||||
@@ -216,9 +202,9 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
|
|||||||
currentSecurityNumber % 1000);
|
currentSecurityNumber % 1000);
|
||||||
cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_inform_tag;
|
cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_inform_tag;
|
||||||
cn->payload_variant.key_verification_number_inform.nonce = currentNonce;
|
cn->payload_variant.key_verification_number_inform.nonce = currentNonce;
|
||||||
copyNodeLongNameOrUnknown(cn->payload_variant.key_verification_number_inform.remote_longname,
|
strncpy(cn->payload_variant.key_verification_number_inform.remote_longname, // should really check for nulls, etc
|
||||||
sizeof(cn->payload_variant.key_verification_number_inform.remote_longname),
|
nodeDB->getMeshNode(currentRemoteNode)->user.long_name,
|
||||||
nodeDB->getMeshNode(currentRemoteNode));
|
sizeof(cn->payload_variant.key_verification_number_inform.remote_longname));
|
||||||
cn->payload_variant.key_verification_number_inform.security_number = currentSecurityNumber;
|
cn->payload_variant.key_verification_number_inform.security_number = currentSecurityNumber;
|
||||||
service->sendClientNotification(cn);
|
service->sendClientNotification(cn);
|
||||||
LOG_WARN("Security Number %04u, nonce %llu", currentSecurityNumber, currentNonce);
|
LOG_WARN("Security Number %04u, nonce %llu", currentSecurityNumber, currentNonce);
|
||||||
@@ -233,7 +219,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
|
|||||||
LOG_WARN("received security number: %u", incomingNumber);
|
LOG_WARN("received security number: %u", incomingNumber);
|
||||||
meshtastic_NodeInfoLite *remoteNodePtr = nullptr;
|
meshtastic_NodeInfoLite *remoteNodePtr = nullptr;
|
||||||
remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
||||||
if (!remoteNodePtr || !nodeInfoLiteHasUser(remoteNodePtr) || remoteNodePtr->public_key.size != 32) {
|
if (remoteNodePtr == nullptr || !remoteNodePtr->has_user || remoteNodePtr->user.public_key.size != 32) {
|
||||||
currentState = KEY_VERIFICATION_IDLE;
|
currentState = KEY_VERIFICATION_IDLE;
|
||||||
return; // should we throw an error here?
|
return; // should we throw an error here?
|
||||||
}
|
}
|
||||||
@@ -246,7 +232,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
|
|||||||
hash.update(¤tRemoteNode, sizeof(currentRemoteNode));
|
hash.update(¤tRemoteNode, sizeof(currentRemoteNode));
|
||||||
hash.update(owner.public_key.bytes, owner.public_key.size);
|
hash.update(owner.public_key.bytes, owner.public_key.size);
|
||||||
|
|
||||||
hash.update(remoteNodePtr->public_key.bytes, remoteNodePtr->public_key.size);
|
hash.update(remoteNodePtr->user.public_key.bytes, remoteNodePtr->user.public_key.size);
|
||||||
hash.finalize(hash1, 32);
|
hash.finalize(hash1, 32);
|
||||||
|
|
||||||
hash.reset();
|
hash.reset();
|
||||||
@@ -279,9 +265,9 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
|
|||||||
sprintf(cn->message, "Final confirmation for outgoing manual key verification %s", message);
|
sprintf(cn->message, "Final confirmation for outgoing manual key verification %s", message);
|
||||||
cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag;
|
cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag;
|
||||||
cn->payload_variant.key_verification_final.nonce = currentNonce;
|
cn->payload_variant.key_verification_final.nonce = currentNonce;
|
||||||
copyNodeLongNameOrUnknown(cn->payload_variant.key_verification_final.remote_longname,
|
strncpy(cn->payload_variant.key_verification_final.remote_longname, // should really check for nulls, etc
|
||||||
sizeof(cn->payload_variant.key_verification_final.remote_longname),
|
nodeDB->getMeshNode(currentRemoteNode)->user.long_name,
|
||||||
nodeDB->getMeshNode(currentRemoteNode));
|
sizeof(cn->payload_variant.key_verification_final.remote_longname));
|
||||||
cn->payload_variant.key_verification_final.isSender = true;
|
cn->payload_variant.key_verification_final.isSender = true;
|
||||||
service->sendClientNotification(cn);
|
service->sendClientNotification(cn);
|
||||||
LOG_INFO(message);
|
LOG_INFO(message);
|
||||||
@@ -324,4 +310,4 @@ void KeyVerificationModule::generateVerificationCode(char *readableCode)
|
|||||||
readableCode[i] = (hash1[i] >> 2) + 48; // not a standardized base64, but workable and avoids having a dictionary.
|
readableCode[i] = (hash1[i] >> 2) + 48; // not a standardized base64, but workable and avoids having a dictionary.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -49,6 +49,7 @@
|
|||||||
#include "modules/WaypointModule.h"
|
#include "modules/WaypointModule.h"
|
||||||
#endif
|
#endif
|
||||||
#if ARCH_PORTDUINO
|
#if ARCH_PORTDUINO
|
||||||
|
#include "modules/DMShell.h"
|
||||||
#include "modules/Telemetry/HostMetrics.h"
|
#include "modules/Telemetry/HostMetrics.h"
|
||||||
#if !MESHTASTIC_EXCLUDE_STOREFORWARD
|
#if !MESHTASTIC_EXCLUDE_STOREFORWARD
|
||||||
#include "modules/StoreForwardModule.h"
|
#include "modules/StoreForwardModule.h"
|
||||||
@@ -195,6 +196,7 @@ void setupModules()
|
|||||||
#endif
|
#endif
|
||||||
#if ARCH_PORTDUINO
|
#if ARCH_PORTDUINO
|
||||||
new HostMetricsModule();
|
new HostMetricsModule();
|
||||||
|
dmShellModule = new DMShellModule();
|
||||||
#endif
|
#endif
|
||||||
#if HAS_TELEMETRY
|
#if HAS_TELEMETRY
|
||||||
new DeviceTelemetryModule();
|
new DeviceTelemetryModule();
|
||||||
|
|||||||
@@ -183,7 +183,8 @@ meshtastic_MeshPacket *PositionModule::allocPositionPacket()
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
const meshtastic_NodeInfoLite *node = service->refreshLocalMeshNode(); // should guarantee there is now a position
|
meshtastic_NodeInfoLite *node = service->refreshLocalMeshNode(); // should guarantee there is now a position
|
||||||
|
assert(node->has_position);
|
||||||
|
|
||||||
// configuration of POSITION packet
|
// configuration of POSITION packet
|
||||||
// consider making this a function argument?
|
// consider making this a function argument?
|
||||||
@@ -193,9 +194,7 @@ meshtastic_MeshPacket *PositionModule::allocPositionPacket()
|
|||||||
meshtastic_Position p = meshtastic_Position_init_default; // Start with an empty structure
|
meshtastic_Position p = meshtastic_Position_init_default; // Start with an empty structure
|
||||||
// if localPosition is totally empty, put our last saved position (lite) in there
|
// if localPosition is totally empty, put our last saved position (lite) in there
|
||||||
if (localPosition.latitude_i == 0 && localPosition.longitude_i == 0) {
|
if (localPosition.latitude_i == 0 && localPosition.longitude_i == 0) {
|
||||||
meshtastic_PositionLite cachedSelf;
|
nodeDB->setLocalPosition(TypeConversions::ConvertToPosition(node->position));
|
||||||
if (nodeDB->copyNodePosition(node->num, cachedSelf))
|
|
||||||
nodeDB->setLocalPosition(TypeConversions::ConvertToPosition(cachedSelf));
|
|
||||||
}
|
}
|
||||||
localPosition.seq_number++;
|
localPosition.seq_number++;
|
||||||
|
|
||||||
@@ -421,7 +420,7 @@ int32_t PositionModule::runOnce()
|
|||||||
doDeepSleep(nightyNightMs, false, false);
|
doDeepSleep(nightyNightMs, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||||
if (node == nullptr)
|
if (node == nullptr)
|
||||||
return RUNONCE_INTERVAL;
|
return RUNONCE_INTERVAL;
|
||||||
|
|
||||||
@@ -446,11 +445,8 @@ int32_t PositionModule::runOnce()
|
|||||||
} else if (nodeDB->hasValidPosition(node)) {
|
} else if (nodeDB->hasValidPosition(node)) {
|
||||||
lastGpsSend = now;
|
lastGpsSend = now;
|
||||||
|
|
||||||
meshtastic_PositionLite selfPos;
|
lastGpsLatitude = node->position.latitude_i;
|
||||||
if (nodeDB->copyNodePosition(node->num, selfPos)) {
|
lastGpsLongitude = node->position.longitude_i;
|
||||||
lastGpsLatitude = selfPos.latitude_i;
|
|
||||||
lastGpsLongitude = selfPos.longitude_i;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (transmitHistory)
|
if (transmitHistory)
|
||||||
transmitHistory->setLastSentToMesh(meshtastic_PortNum_POSITION_APP);
|
transmitHistory->setLastSentToMesh(meshtastic_PortNum_POSITION_APP);
|
||||||
@@ -465,10 +461,7 @@ int32_t PositionModule::runOnce()
|
|||||||
if (nodeDB->hasValidPosition(node2)) {
|
if (nodeDB->hasValidPosition(node2)) {
|
||||||
// The minimum time (in seconds) that would pass before we are able to send a new position packet.
|
// The minimum time (in seconds) that would pass before we are able to send a new position packet.
|
||||||
|
|
||||||
meshtastic_PositionLite selfPos;
|
auto smartPosition = getDistanceTraveledSinceLastSend(node->position);
|
||||||
if (!nodeDB->copyNodePosition(node->num, selfPos))
|
|
||||||
return RUNONCE_INTERVAL; // Defensive: hasValidPosition should imply this is non-null
|
|
||||||
auto smartPosition = getDistanceTraveledSinceLastSend(selfPos);
|
|
||||||
msSinceLastSend = now - lastGpsSend;
|
msSinceLastSend = now - lastGpsSend;
|
||||||
|
|
||||||
if (smartPosition.hasTraveledOverThreshold &&
|
if (smartPosition.hasTraveledOverThreshold &&
|
||||||
@@ -486,8 +479,8 @@ int32_t PositionModule::runOnce()
|
|||||||
msSinceLastSend, minimumTimeThreshold);
|
msSinceLastSend, minimumTimeThreshold);
|
||||||
|
|
||||||
// Set the current coords as our last ones, after we've compared distance with current and decided to send
|
// Set the current coords as our last ones, after we've compared distance with current and decided to send
|
||||||
lastGpsLatitude = selfPos.latitude_i;
|
lastGpsLatitude = node->position.latitude_i;
|
||||||
lastGpsLongitude = selfPos.longitude_i;
|
lastGpsLongitude = node->position.longitude_i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -570,14 +563,11 @@ struct SmartPosition PositionModule::getDistanceTraveledSinceLastSend(meshtastic
|
|||||||
|
|
||||||
void PositionModule::handleNewPosition()
|
void PositionModule::handleNewPosition()
|
||||||
{
|
{
|
||||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||||
const meshtastic_NodeInfoLite *node2 = service->refreshLocalMeshNode(); // should guarantee there is now a position
|
const meshtastic_NodeInfoLite *node2 = service->refreshLocalMeshNode(); // should guarantee there is now a position
|
||||||
// We limit our GPS broadcasts to a max rate
|
// We limit our GPS broadcasts to a max rate
|
||||||
if (nodeDB->hasValidPosition(node2)) {
|
if (nodeDB->hasValidPosition(node2)) {
|
||||||
meshtastic_PositionLite selfPos;
|
auto smartPosition = getDistanceTraveledSinceLastSend(node->position);
|
||||||
if (!nodeDB->copyNodePosition(node->num, selfPos))
|
|
||||||
return;
|
|
||||||
auto smartPosition = getDistanceTraveledSinceLastSend(selfPos);
|
|
||||||
uint32_t msSinceLastSend = millis() - lastGpsSend;
|
uint32_t msSinceLastSend = millis() - lastGpsSend;
|
||||||
if (smartPosition.hasTraveledOverThreshold &&
|
if (smartPosition.hasTraveledOverThreshold &&
|
||||||
Throttle::execute(
|
Throttle::execute(
|
||||||
@@ -593,8 +583,8 @@ void PositionModule::handleNewPosition()
|
|||||||
minimumTimeThreshold);
|
minimumTimeThreshold);
|
||||||
|
|
||||||
// Set the current coords as our last ones, after we've compared distance with current and decided to send
|
// Set the current coords as our last ones, after we've compared distance with current and decided to send
|
||||||
lastGpsLatitude = selfPos.latitude_i;
|
lastGpsLatitude = node->position.latitude_i;
|
||||||
lastGpsLongitude = selfPos.longitude_i;
|
lastGpsLongitude = node->position.longitude_i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -268,36 +268,26 @@ bool RangeTestModuleRadio::appendFile(const meshtastic_MeshPacket &mp)
|
|||||||
fileToAppend.printf("??:??:??,"); // Time
|
fileToAppend.printf("??:??:??,"); // Time
|
||||||
}
|
}
|
||||||
|
|
||||||
fileToAppend.printf("%d,", getFrom(&mp)); // From
|
fileToAppend.printf("%d,", getFrom(&mp)); // From
|
||||||
fileToAppend.printf("%s,", n ? n->long_name : ""); // Long Name
|
fileToAppend.printf("%s,", n->user.long_name); // Long Name
|
||||||
meshtastic_PositionLite senderPos;
|
fileToAppend.printf("%f,", n->position.latitude_i * 1e-7); // Sender Lat
|
||||||
const bool haveSenderPos = nodeDB->copyNodePosition(getFrom(&mp), senderPos);
|
fileToAppend.printf("%f,", n->position.longitude_i * 1e-7); // Sender Long
|
||||||
if (haveSenderPos) {
|
|
||||||
fileToAppend.printf("%f,", senderPos.latitude_i * 1e-7); // Sender Lat
|
|
||||||
fileToAppend.printf("%f,", senderPos.longitude_i * 1e-7); // Sender Long
|
|
||||||
} else {
|
|
||||||
fileToAppend.printf("0.0,0.0,");
|
|
||||||
}
|
|
||||||
if (gpsStatus->getIsConnected() || config.position.fixed_position) {
|
if (gpsStatus->getIsConnected() || config.position.fixed_position) {
|
||||||
fileToAppend.printf("%f,", gpsStatus->getLatitude() * 1e-7); // RX Lat
|
fileToAppend.printf("%f,", gpsStatus->getLatitude() * 1e-7); // RX Lat
|
||||||
fileToAppend.printf("%f,", gpsStatus->getLongitude() * 1e-7); // RX Long
|
fileToAppend.printf("%f,", gpsStatus->getLongitude() * 1e-7); // RX Long
|
||||||
fileToAppend.printf("%d,", gpsStatus->getAltitude()); // RX Altitude
|
fileToAppend.printf("%d,", gpsStatus->getAltitude()); // RX Altitude
|
||||||
} else {
|
} else {
|
||||||
// When the phone API is in use, the node info will be updated with position
|
// When the phone API is in use, the node info will be updated with position
|
||||||
meshtastic_PositionLite usPos;
|
meshtastic_NodeInfoLite *us = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||||
if (nodeDB->copyNodePosition(nodeDB->getNodeNum(), usPos)) {
|
fileToAppend.printf("%f,", us->position.latitude_i * 1e-7); // RX Lat
|
||||||
fileToAppend.printf("%f,", usPos.latitude_i * 1e-7); // RX Lat
|
fileToAppend.printf("%f,", us->position.longitude_i * 1e-7); // RX Long
|
||||||
fileToAppend.printf("%f,", usPos.longitude_i * 1e-7); // RX Long
|
fileToAppend.printf("%d,", us->position.altitude); // RX Altitude
|
||||||
fileToAppend.printf("%d,", usPos.altitude); // RX Altitude
|
|
||||||
} else {
|
|
||||||
fileToAppend.printf("0.0,0.0,0,");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fileToAppend.printf("%f,", mp.rx_snr); // RX SNR
|
fileToAppend.printf("%f,", mp.rx_snr); // RX SNR
|
||||||
|
|
||||||
if (haveSenderPos && senderPos.latitude_i && senderPos.longitude_i && gpsStatus->getLatitude() && gpsStatus->getLongitude()) {
|
if (n->position.latitude_i && n->position.longitude_i && gpsStatus->getLatitude() && gpsStatus->getLongitude()) {
|
||||||
float distance = GeoCoord::latLongToMeter(senderPos.latitude_i * 1e-7, senderPos.longitude_i * 1e-7,
|
float distance = GeoCoord::latLongToMeter(n->position.latitude_i * 1e-7, n->position.longitude_i * 1e-7,
|
||||||
gpsStatus->getLatitude() * 1e-7, gpsStatus->getLongitude() * 1e-7);
|
gpsStatus->getLatitude() * 1e-7, gpsStatus->getLongitude() * 1e-7);
|
||||||
fileToAppend.printf("%f,", distance); // Distance in meters
|
fileToAppend.printf("%f,", distance); // Distance in meters
|
||||||
} else {
|
} else {
|
||||||
@@ -350,4 +340,4 @@ bool RangeTestModuleRadio::removeFile()
|
|||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,8 @@ bool RoutingModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mesh
|
|||||||
config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_KNOWN_ONLY)) {
|
config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_KNOWN_ONLY)) {
|
||||||
if (!maybePKI)
|
if (!maybePKI)
|
||||||
return false;
|
return false;
|
||||||
if (!nodeInfoLiteHasUser(nodeDB->getMeshNode(mp.from)) && !nodeInfoLiteHasUser(nodeDB->getMeshNode(mp.to)))
|
if ((nodeDB->getMeshNode(mp.from) == NULL || !nodeDB->getMeshNode(mp.from)->has_user) &&
|
||||||
|
(nodeDB->getMeshNode(mp.to) == NULL || !nodeDB->getMeshNode(mp.to)->has_user))
|
||||||
return false;
|
return false;
|
||||||
} else if (owner.is_licensed && ((nodeDB->getLicenseStatus(mp.from) == UserLicenseStatus::NotLicensed) ||
|
} else if (owner.is_licensed && ((nodeDB->getLicenseStatus(mp.from) == UserLicenseStatus::NotLicensed) ||
|
||||||
(nodeDB->getLicenseStatus(mp.to) == UserLicenseStatus::NotLicensed))) {
|
(nodeDB->getLicenseStatus(mp.to) == UserLicenseStatus::NotLicensed))) {
|
||||||
|
|||||||
@@ -250,12 +250,9 @@ int32_t SerialModule::runOnce()
|
|||||||
uint32_t readIndex = 0;
|
uint32_t readIndex = 0;
|
||||||
const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
|
const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
|
||||||
while (tempNodeInfo != NULL) {
|
while (tempNodeInfo != NULL) {
|
||||||
if (nodeInfoLiteHasUser(tempNodeInfo) && nodeDB->hasValidPosition(tempNodeInfo)) {
|
if (tempNodeInfo->has_user && nodeDB->hasValidPosition(tempNodeInfo)) {
|
||||||
meshtastic_PositionLite pos;
|
printWPL(outbuf, sizeof(outbuf), tempNodeInfo->position, tempNodeInfo->user.long_name, true);
|
||||||
if (nodeDB->copyNodePosition(tempNodeInfo->num, pos)) {
|
serialPrint->printf("%s", outbuf);
|
||||||
printWPL(outbuf, sizeof(outbuf), pos, tempNodeInfo->long_name, true);
|
|
||||||
serialPrint->printf("%s", outbuf);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
|
tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
|
||||||
}
|
}
|
||||||
@@ -404,7 +401,7 @@ ProcessMessage SerialModuleRadio::handleReceived(const meshtastic_MeshPacket &mp
|
|||||||
serialPrint->write(p.payload.bytes, p.payload.size);
|
serialPrint->write(p.payload.bytes, p.payload.size);
|
||||||
} else if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG) {
|
} else if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG) {
|
||||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp));
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp));
|
||||||
const char *sender = nodeInfoLiteHasUser(node) ? node->short_name : "???";
|
const char *sender = (node && node->has_user) ? node->user.short_name : "???";
|
||||||
serialPrint->println();
|
serialPrint->println();
|
||||||
serialPrint->printf("%s: %s", sender, p.payload.bytes);
|
serialPrint->printf("%s: %s", sender, p.payload.bytes);
|
||||||
serialPrint->println();
|
serialPrint->println();
|
||||||
@@ -420,13 +417,9 @@ ProcessMessage SerialModuleRadio::handleReceived(const meshtastic_MeshPacket &mp
|
|||||||
decoded = &scratch;
|
decoded = &scratch;
|
||||||
}
|
}
|
||||||
// send position packet as WPL to the serial port
|
// send position packet as WPL to the serial port
|
||||||
{
|
printWPL(outbuf, sizeof(outbuf), *decoded, nodeDB->getMeshNode(getFrom(&mp))->user.long_name,
|
||||||
meshtastic_NodeInfoLite *senderNode = nodeDB->getMeshNode(getFrom(&mp));
|
moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO);
|
||||||
const char *senderName = senderNode ? senderNode->long_name : "";
|
serialPrint->printf("%s", outbuf);
|
||||||
printWPL(outbuf, sizeof(outbuf), *decoded, senderName,
|
|
||||||
moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO);
|
|
||||||
serialPrint->printf("%s", outbuf);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -721,4 +714,4 @@ void SerialModule::processWXSerial()
|
|||||||
#endif
|
#endif
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
#include "StatusMessageModule.h"
|
#include "StatusMessageModule.h"
|
||||||
#include "MeshService.h"
|
#include "MeshService.h"
|
||||||
#include "NodeDB.h"
|
|
||||||
#include "ProtobufModule.h"
|
#include "ProtobufModule.h"
|
||||||
|
|
||||||
StatusMessageModule *statusMessageModule;
|
StatusMessageModule *statusMessageModule;
|
||||||
@@ -37,8 +36,16 @@ ProcessMessage StatusMessageModule::handleReceived(const meshtastic_MeshPacket &
|
|||||||
|
|
||||||
LOG_INFO("Received a NodeStatus message %s", incomingMessage.status);
|
LOG_INFO("Received a NodeStatus message %s", incomingMessage.status);
|
||||||
|
|
||||||
if (nodeDB)
|
RecentStatus entry;
|
||||||
nodeDB->setNodeStatus(mp.from, incomingMessage);
|
entry.fromNodeId = mp.from;
|
||||||
|
entry.statusText = incomingMessage.status;
|
||||||
|
|
||||||
|
recentReceived.push_back(std::move(entry));
|
||||||
|
|
||||||
|
// Keep only last MAX_RECENT_STATUSMESSAGES
|
||||||
|
if (recentReceived.size() > MAX_RECENT_STATUSMESSAGES) {
|
||||||
|
recentReceived.erase(recentReceived.begin()); // drop oldest
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ProcessMessage::CONTINUE;
|
return ProcessMessage::CONTINUE;
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||||
#include "SinglePortModule.h"
|
#include "SinglePortModule.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
class StatusMessageModule : public SinglePortModule, private concurrency::OSThread
|
class StatusMessageModule : public SinglePortModule, private concurrency::OSThread
|
||||||
{
|
{
|
||||||
@@ -18,16 +20,29 @@ class StatusMessageModule : public SinglePortModule, private concurrency::OSThre
|
|||||||
this->setInterval(1000 * 12 * 60 * 60);
|
this->setInterval(1000 * 12 * 60 * 60);
|
||||||
}
|
}
|
||||||
// TODO: If we have a string, set the initial delay (15 minutes maybe)
|
// TODO: If we have a string, set the initial delay (15 minutes maybe)
|
||||||
|
|
||||||
|
// Keep vector from reallocating as we fill up to MAX_RECENT_STATUSMESSAGES
|
||||||
|
recentReceived.reserve(MAX_RECENT_STATUSMESSAGES);
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual int32_t runOnce() override;
|
virtual int32_t runOnce() override;
|
||||||
|
|
||||||
|
struct RecentStatus {
|
||||||
|
uint32_t fromNodeId; // mp.from
|
||||||
|
std::string statusText; // incomingMessage.status
|
||||||
|
};
|
||||||
|
|
||||||
|
const std::vector<RecentStatus> &getRecentReceived() const { return recentReceived; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
/** Called to handle a particular incoming message. The cached most-recent
|
/** Called to handle a particular incoming message
|
||||||
* status for each node lives on NodeDB; use nodeDB->copyNodeStatus(num, ...).
|
|
||||||
*/
|
*/
|
||||||
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr size_t MAX_RECENT_STATUSMESSAGES = 5;
|
||||||
|
std::vector<RecentStatus> recentReceived;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern StatusMessageModule *statusMessageModule;
|
extern StatusMessageModule *statusMessageModule;
|
||||||
#endif
|
#endif
|
||||||
@@ -80,9 +80,9 @@ void StoreForwardModule::populatePSRAM()
|
|||||||
(this->records ? this->records : (((memGet.getFreePsram() / 4) * 3) / sizeof(PacketHistoryStruct)));
|
(this->records ? this->records : (((memGet.getFreePsram() / 4) * 3) / sizeof(PacketHistoryStruct)));
|
||||||
this->records = numberOfPackets;
|
this->records = numberOfPackets;
|
||||||
#if defined(ARCH_ESP32)
|
#if defined(ARCH_ESP32)
|
||||||
this->packetHistory.reset(static_cast<PacketHistoryStruct *>(ps_calloc(numberOfPackets, sizeof(PacketHistoryStruct))));
|
this->packetHistory = static_cast<PacketHistoryStruct *>(ps_calloc(numberOfPackets, sizeof(PacketHistoryStruct)));
|
||||||
#elif defined(ARCH_PORTDUINO)
|
#elif defined(ARCH_PORTDUINO)
|
||||||
this->packetHistory.reset(static_cast<PacketHistoryStruct *>(calloc(numberOfPackets, sizeof(PacketHistoryStruct))));
|
this->packetHistory = static_cast<PacketHistoryStruct *>(calloc(numberOfPackets, sizeof(PacketHistoryStruct)));
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
struct PacketHistoryStruct {
|
struct PacketHistoryStruct {
|
||||||
@@ -30,17 +29,11 @@ struct PacketHistoryStruct {
|
|||||||
|
|
||||||
class StoreForwardModule : private concurrency::OSThread, public ProtobufModule<meshtastic_StoreAndForward>
|
class StoreForwardModule : private concurrency::OSThread, public ProtobufModule<meshtastic_StoreAndForward>
|
||||||
{
|
{
|
||||||
// packetHistory is allocated with ps_calloc / calloc, so it must be released with free(),
|
|
||||||
// not delete[].
|
|
||||||
struct CFreeDeleter {
|
|
||||||
void operator()(PacketHistoryStruct *p) const noexcept { free(p); }
|
|
||||||
};
|
|
||||||
|
|
||||||
bool busy = 0;
|
bool busy = 0;
|
||||||
uint32_t busyTo = 0;
|
uint32_t busyTo = 0;
|
||||||
char routerMessage[meshtastic_Constants_DATA_PAYLOAD_LEN] = {0};
|
char routerMessage[meshtastic_Constants_DATA_PAYLOAD_LEN] = {0};
|
||||||
|
|
||||||
std::unique_ptr<PacketHistoryStruct[], CFreeDeleter> packetHistory;
|
PacketHistoryStruct *packetHistory = 0;
|
||||||
uint32_t packetHistoryTotalCount = 0;
|
uint32_t packetHistoryTotalCount = 0;
|
||||||
uint32_t last_time = 0;
|
uint32_t last_time = 0;
|
||||||
uint32_t requestCount = 0;
|
uint32_t requestCount = 0;
|
||||||
|
|||||||
@@ -515,10 +515,6 @@ bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPac
|
|||||||
packetPool.release(lastMeasurementPacket);
|
packetPool.release(lastMeasurementPacket);
|
||||||
|
|
||||||
lastMeasurementPacket = packetPool.allocCopy(mp);
|
lastMeasurementPacket = packetPool.allocCopy(mp);
|
||||||
|
|
||||||
// Cache the latest env metrics per node on NodeDB so the phone can
|
|
||||||
// pull last-known values across reboots and replays.
|
|
||||||
nodeDB->updateTelemetry(getFrom(&mp), *t, RX_SRC_RADIO);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false; // Let others look at this message also if they want
|
return false; // Let others look at this message also if they want
|
||||||
|
|||||||
@@ -16,28 +16,10 @@ int32_t INA3221Sensor::runOnce()
|
|||||||
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
|
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
|
||||||
}
|
}
|
||||||
if (!status) {
|
if (!status) {
|
||||||
// Re-initialise with the address and Wire bus from the telemetry sensors map.
|
ina3221.begin(nodeTelemetrySensorsMap[sensorType].second);
|
||||||
// (Rob Tillaart INA3221_RT takes address + TwoWire*, unlike sgtwilko which took Wire in begin().)
|
ina3221.setShuntRes(100, 100, 100); // 0.1 Ohm shunt resistors
|
||||||
ina3221 = INA3221(nodeTelemetrySensorsMap[sensorType].first, nodeTelemetrySensorsMap[sensorType].second);
|
status = true;
|
||||||
status = ina3221.begin();
|
|
||||||
if (status) {
|
|
||||||
// Default all three channels to a 0.1 Ω shunt resistor.
|
|
||||||
// Override per-variant by defining INA3221_SHUNT_R_CH1/CH2/CH3 (in Ohms) in variant.h.
|
|
||||||
#ifndef INA3221_SHUNT_R_CH1
|
|
||||||
#define INA3221_SHUNT_R_CH1 0.1f
|
|
||||||
#endif
|
|
||||||
#ifndef INA3221_SHUNT_R_CH2
|
|
||||||
#define INA3221_SHUNT_R_CH2 0.1f
|
|
||||||
#endif
|
|
||||||
#ifndef INA3221_SHUNT_R_CH3
|
|
||||||
#define INA3221_SHUNT_R_CH3 0.1f
|
|
||||||
#endif
|
|
||||||
ina3221.setShuntR(0, INA3221_SHUNT_R_CH1);
|
|
||||||
ina3221.setShuntR(1, INA3221_SHUNT_R_CH2);
|
|
||||||
ina3221.setShuntR(2, INA3221_SHUNT_R_CH3);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Already initialised; status stays true and initI2CSensor() returns next poll interval.
|
|
||||||
status = true;
|
status = true;
|
||||||
}
|
}
|
||||||
return initI2CSensor();
|
return initI2CSensor();
|
||||||
@@ -45,14 +27,12 @@ int32_t INA3221Sensor::runOnce()
|
|||||||
|
|
||||||
void INA3221Sensor::setup() {}
|
void INA3221Sensor::setup() {}
|
||||||
|
|
||||||
struct _INA3221Measurement INA3221Sensor::getMeasurement(uint8_t ch)
|
struct _INA3221Measurement INA3221Sensor::getMeasurement(ina3221_ch_t ch)
|
||||||
{
|
{
|
||||||
struct _INA3221Measurement measurement;
|
struct _INA3221Measurement measurement;
|
||||||
|
|
||||||
measurement.voltage = ina3221.getBusVoltage(ch); // Volts
|
measurement.voltage = ina3221.getVoltage(ch);
|
||||||
// getCurrent_mA() is used instead of getCurrent() because Rob Tillaart's getCurrent()
|
measurement.current = ina3221.getCurrent(ch);
|
||||||
// returns Amperes; the telemetry proto and VoltageSensor/CurrentSensor interfaces expect mA.
|
|
||||||
measurement.current = ina3221.getCurrent_mA(ch); // milliAmps
|
|
||||||
|
|
||||||
return measurement;
|
return measurement;
|
||||||
}
|
}
|
||||||
@@ -63,7 +43,7 @@ struct _INA3221Measurements INA3221Sensor::getMeasurements()
|
|||||||
|
|
||||||
// INA3221 has 3 channels starting from 0
|
// INA3221 has 3 channels starting from 0
|
||||||
for (int i = 0; i < 3; i++) {
|
for (int i = 0; i < 3; i++) {
|
||||||
measurements.measurements[i] = getMeasurement((uint8_t)i);
|
measurements.measurements[i] = getMeasurement((ina3221_ch_t)i);
|
||||||
}
|
}
|
||||||
|
|
||||||
return measurements;
|
return measurements;
|
||||||
@@ -107,41 +87,24 @@ bool INA3221Sensor::getPowerMetrics(meshtastic_Telemetry *measurement)
|
|||||||
measurement->variant.power_metrics.has_ch3_voltage = true;
|
measurement->variant.power_metrics.has_ch3_voltage = true;
|
||||||
measurement->variant.power_metrics.has_ch3_current = true;
|
measurement->variant.power_metrics.has_ch3_current = true;
|
||||||
|
|
||||||
// INA3221 channel indices are zero-based (0=CH1, 1=CH2, 2=CH3).
|
measurement->variant.power_metrics.ch1_voltage = m.measurements[INA3221_CH1].voltage;
|
||||||
measurement->variant.power_metrics.ch1_voltage = m.measurements[0].voltage;
|
measurement->variant.power_metrics.ch1_current = m.measurements[INA3221_CH1].current;
|
||||||
measurement->variant.power_metrics.ch1_current = m.measurements[0].current;
|
measurement->variant.power_metrics.ch2_voltage = m.measurements[INA3221_CH2].voltage;
|
||||||
measurement->variant.power_metrics.ch2_voltage = m.measurements[1].voltage;
|
measurement->variant.power_metrics.ch2_current = m.measurements[INA3221_CH2].current;
|
||||||
measurement->variant.power_metrics.ch2_current = m.measurements[1].current;
|
measurement->variant.power_metrics.ch3_voltage = m.measurements[INA3221_CH3].voltage;
|
||||||
measurement->variant.power_metrics.ch3_voltage = m.measurements[2].voltage;
|
measurement->variant.power_metrics.ch3_current = m.measurements[INA3221_CH3].current;
|
||||||
measurement->variant.power_metrics.ch3_current = m.measurements[2].current;
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t INA3221Sensor::getBusVoltageMv()
|
uint16_t INA3221Sensor::getBusVoltageMv()
|
||||||
{
|
{
|
||||||
return lround(ina3221.getBusVoltage_mV(BAT_CH));
|
return lround(ina3221.getVoltage(BAT_CH) * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
int16_t INA3221Sensor::getCurrentMa()
|
int16_t INA3221Sensor::getCurrentMa()
|
||||||
{
|
{
|
||||||
return lround(ina3221.getCurrent_mA(BAT_CH));
|
return lround(ina3221.getCurrent(BAT_CH));
|
||||||
}
|
|
||||||
|
|
||||||
// Bus voltage register (0x02 + ch*2): bits [15:3] unsigned, 1 LSB = 8 mV (datasheet p.6).
|
|
||||||
// Voltage raw units: 1 count = 8 mV, so V_mV = raw * 8.
|
|
||||||
int16_t INA3221Sensor::getRawBusVoltage(uint8_t ch)
|
|
||||||
{
|
|
||||||
return (int16_t)(ina3221.getRegister(0x02 + ch * 2) >> 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shunt voltage register (0x01 + ch*2): bits [15:3] signed two's complement, 1 LSB = 40 µV (datasheet p.6).
|
|
||||||
// Current raw units are shunt-voltage counts: 1 count = 40 uV, signed.
|
|
||||||
// I_mA = (raw * 40 uV) / R_mOhm, because uV / mOhm = mA.
|
|
||||||
// Example for 100 mOhm shunt: I_mA = raw * 40 / 100 = raw * 0.4.
|
|
||||||
int16_t INA3221Sensor::getRawShuntCurrent(uint8_t ch)
|
|
||||||
{
|
|
||||||
return (int16_t)(ina3221.getRegister(0x01 + ch * 2) >> 3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user