mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-05 03:38:32 +10:00
Compare commits
63
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39b19f7f8c | ||
|
|
9c2b8706de | ||
|
|
359f8d029e | ||
|
|
daa1f69671 | ||
|
|
90164b272e | ||
|
|
1e965367e6 | ||
|
|
27ecd77c1a | ||
|
|
6208c243f9 | ||
|
|
3a87e746a8 | ||
|
|
cd04206334 | ||
|
|
8e55a6e4c1 | ||
|
|
f14ef121ef | ||
|
|
92263859a7 | ||
|
|
2768080edf | ||
|
|
466cc4cecd | ||
|
|
fe90c49795 | ||
|
|
23f92c1cbd | ||
|
|
31418ca821 | ||
|
|
0ee5777c15 | ||
|
|
026213aab7 | ||
|
|
c1bee82baf | ||
|
|
0cab43fb43 | ||
|
|
47e129f4bd | ||
|
|
1341cd4078 | ||
|
|
4059202a5c | ||
|
|
d24d8806e1 | ||
|
|
a67eb15ad3 | ||
|
|
9e182a595c | ||
|
|
4587dc2d64 | ||
|
|
00fccd87f9 | ||
|
|
01bd4cfb73 | ||
|
|
125c1b7f13 | ||
|
|
d6cf67b6bc | ||
|
|
4e000ee4ce | ||
|
|
0f7a6d8b3b | ||
|
|
2f5dc2ace6 | ||
|
|
800b3d903e | ||
|
|
96dd647882 | ||
|
|
2d754b8e4b | ||
|
|
84ac28a607 | ||
|
|
65c18eb26b | ||
|
|
77f378dd53 | ||
|
|
e42ff3590c | ||
|
|
7527233130 | ||
|
|
197226365b | ||
|
|
16dcafa7fb | ||
|
|
eeb95d6bff | ||
|
|
60fb8eb0d4 | ||
|
|
4ac74edf38 | ||
|
|
323830c7cc | ||
|
|
a9acd506a8 | ||
|
|
a1d6c6db62 | ||
|
|
69495dcd98 | ||
|
|
0f2d224e74 | ||
|
|
381cefa6b2 | ||
|
|
a50cbdc95b | ||
|
|
e7c7af40ae | ||
|
|
48ae4b6c7a | ||
|
|
ae5019bec6 | ||
|
|
3cd3fd3386 | ||
|
|
839cf554b7 | ||
|
|
216655f05f | ||
|
|
2c633b6458 |
@@ -18,7 +18,7 @@ ENV PIP_ROOT_USER_ACTION=ignore
|
||||
# trunk-ignore(hadolint/DL3008): apt packages are not pinned.
|
||||
# trunk-ignore(terrascan/AC_DOCKER_0002): apt packages are not pinned.
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||
cmake git zip libgpiod-dev libbluetooth-dev libi2c-dev \
|
||||
cmake git zip libgpiod-dev libjsoncpp-dev libbluetooth-dev libi2c-dev \
|
||||
libunistring-dev libmicrohttpd-dev libgnutls28-dev libgcrypt20-dev \
|
||||
libusb-1.0-0-dev libssl-dev pkg-config libsqlite3-dev libsdl2-dev && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/* && \
|
||||
|
||||
@@ -31,7 +31,7 @@ cmake --install "$WORK/ulfius/$SANITIZER" --prefix /usr
|
||||
cd "$SRC/firmware"
|
||||
|
||||
PLATFORMIO_EXTRA_SCRIPTS=$(echo -e "pre:.clusterfuzzlite/platformio-clusterfuzzlite-pre.py\npost:.clusterfuzzlite/platformio-clusterfuzzlite-post.py")
|
||||
STATIC_LIBS=$(pkg-config --libs --static libulfius openssl libgpiod yaml-cpp bluez --silence-errors)
|
||||
STATIC_LIBS=$(pkg-config --libs --static libulfius openssl libgpiod yaml-cpp jsoncpp bluez --silence-errors)
|
||||
export PLATFORMIO_EXTRA_SCRIPTS
|
||||
export STATIC_LIBS
|
||||
export PLATFORMIO_WORKSPACE_DIR="$WORK/pio/$SANITIZER"
|
||||
|
||||
@@ -16,6 +16,7 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
libssl-dev \
|
||||
libulfius-dev \
|
||||
libyaml-cpp-dev \
|
||||
libjsoncpp-dev \
|
||||
pipx \
|
||||
pkg-config \
|
||||
python3 \
|
||||
|
||||
@@ -13,7 +13,7 @@ runs:
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get -y update --fix-missing
|
||||
sudo apt-get install -y cppcheck libbluetooth-dev libgpiod-dev libyaml-cpp-dev lsb-release
|
||||
sudo apt-get install -y cppcheck libbluetooth-dev libgpiod-dev libyaml-cpp-dev libjsoncpp-dev lsb-release
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
|
||||
@@ -11,4 +11,4 @@ runs:
|
||||
- name: Install libs needed for native build
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get install -y libbluetooth-dev libgpiod-dev libyaml-cpp-dev openssl libssl-dev libulfius-dev liborcania-dev libusb-1.0-0-dev libi2c-dev libuv1-dev
|
||||
sudo apt-get install -y libbluetooth-dev libgpiod-dev libyaml-cpp-dev libjsoncpp-dev openssl libssl-dev libulfius-dev liborcania-dev libusb-1.0-0-dev libi2c-dev libuv1-dev
|
||||
|
||||
+164
-27
@@ -4,11 +4,11 @@ This document provides context and guidelines for AI assistants working with the
|
||||
|
||||
## Project Overview
|
||||
|
||||
Meshtastic is an open-source LoRa mesh networking project for long-range, low-power communication without relying on internet or cellular infrastructure. The firmware enables text messaging, location sharing, and telemetry over a decentralized mesh network.
|
||||
Meshtastic is an open-source LoRa mesh networking project for long-range, low-power communication without relying on internet or cellular infrastructure. The firmware enables text messaging, location sharing, and telemetry over a decentralized mesh network. The project uses **C++17** as its language standard across all platforms.
|
||||
|
||||
### Supported Hardware Platforms
|
||||
|
||||
- **ESP32** (ESP32, ESP32-S3, ESP32-C3) - Most common platform
|
||||
- **ESP32** (ESP32, ESP32-S3, ESP32-C3, ESP32-C6) - Most common platform
|
||||
- **nRF52** (nRF52840, nRF52833) - Low power Nordic chips
|
||||
- **RP2040/RP2350** - Raspberry Pi Pico variants
|
||||
- **STM32WL** - STM32 with integrated LoRa
|
||||
@@ -80,21 +80,46 @@ firmware/
|
||||
│ │ ├── NodeDB.* # Node database management
|
||||
│ │ ├── Router.* # Packet routing
|
||||
│ │ ├── Channels.* # Channel management
|
||||
│ │ ├── CryptoEngine.* # AES-CCM encryption
|
||||
│ │ ├── *Interface.* # Radio interface implementations
|
||||
│ │ ├── api/ # WiFi/Ethernet server APIs (ServerAPI, PacketAPI)
|
||||
│ │ ├── http/ # HTTP server (WebServer, ContentHandler)
|
||||
│ │ ├── wifi/ # WiFi support (WiFiAPClient)
|
||||
│ │ ├── eth/ # Ethernet support (ethClient)
|
||||
│ │ ├── udp/ # UDP multicast
|
||||
│ │ ├── compression/ # Message compression (unishox2)
|
||||
│ │ └── generated/ # Protobuf generated code
|
||||
│ ├── modules/ # Feature modules (Position, Telemetry, etc.)
|
||||
│ │ └── Telemetry/ # Telemetry subsystem
|
||||
│ │ └── Sensor/ # 50+ I2C sensor drivers
|
||||
│ ├── gps/ # GPS handling
|
||||
│ ├── graphics/ # Display drivers and UI
|
||||
│ ├── platform/ # Platform-specific code
|
||||
│ ├── input/ # Input device handling
|
||||
│ └── concurrency/ # Threading utilities
|
||||
│ │ └── niche/ # Specialized UIs (InkHUD e-ink framework)
|
||||
│ ├── platform/ # Platform-specific code (esp32, nrf52, rp2xx0, stm32wl, portduino)
|
||||
│ ├── input/ # Input device handling (InputBroker, keyboards, buttons)
|
||||
│ ├── detect/ # I2C hardware auto-detection (80+ device types)
|
||||
│ ├── motion/ # Accelerometer drivers (BMA423, BMI270, MPU6050, etc.)
|
||||
│ ├── mqtt/ # MQTT bridge client
|
||||
│ ├── power/ # Power HAL
|
||||
│ ├── nimble/ # BLE via NimBLE
|
||||
│ ├── buzz/ # Audio/notification (buzzer, RTTTL)
|
||||
│ ├── serialization/ # JSON serialization, COBS encoding
|
||||
│ ├── watchdog/ # Hardware watchdog thread
|
||||
│ ├── concurrency/ # Threading utilities (OSThread, Lock)
|
||||
│ ├── PowerFSM.* # Power finite state machine
|
||||
│ └── Observer.h # Observer/Observable event pattern
|
||||
├── variants/ # Hardware variant definitions
|
||||
│ ├── esp32/ # ESP32 variants
|
||||
│ ├── esp32s3/ # ESP32-S3 variants
|
||||
│ ├── nrf52/ # nRF52 variants
|
||||
│ └── rp2xxx/ # RP2040/RP2350 variants
|
||||
│ ├── esp32c3/ # ESP32-C3 variants
|
||||
│ ├── esp32c6/ # ESP32-C6 variants
|
||||
│ ├── nrf52840/ # nRF52 variants
|
||||
│ ├── rp2040/ # RP2040/RP2350 variants
|
||||
│ ├── stm32/ # STM32WL variants
|
||||
│ └── native/ # Linux/Portduino variants
|
||||
├── protobufs/ # Protocol buffer definitions
|
||||
├── boards/ # Custom PlatformIO board definitions
|
||||
├── test/ # Unit tests (12 test suites)
|
||||
└── bin/ # Build and utility scripts
|
||||
```
|
||||
|
||||
@@ -105,6 +130,7 @@ firmware/
|
||||
- Follow existing code style - run `trunk fmt` before commits
|
||||
- Prefer `LOG_DEBUG`, `LOG_INFO`, `LOG_WARN`, `LOG_ERROR` for logging
|
||||
- Use `assert()` for invariants that should never fail
|
||||
- C++17 features are available (`std::optional`, structured bindings, `if constexpr`, etc.)
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
@@ -118,70 +144,151 @@ firmware/
|
||||
|
||||
#### Module System
|
||||
|
||||
Modules inherit from `MeshModule` or `ProtobufModule<T>` and implement:
|
||||
Modules use a three-tier class hierarchy:
|
||||
|
||||
- `handleReceivedProtobuf()` - Process incoming packets
|
||||
- `allocReply()` - Generate response packets
|
||||
- `runOnce()` - Periodic task execution (returns next run interval in ms)
|
||||
1. **`MeshModule`** - Base class. Implement `wantPacket()` and `handleReceived()`. Returns `ProcessMessage::STOP` or `ProcessMessage::CONTINUE`.
|
||||
2. **`SinglePortModule`** - Handles a single portnum. Simplified `wantPacket()` that checks `decoded.portnum`.
|
||||
3. **`ProtobufModule<T>`** - Template for protobuf-based modules. Handles encoding/decoding automatically.
|
||||
|
||||
Most modules also inherit from **`OSThread`** for periodic tasks (the "mixin" pattern):
|
||||
|
||||
```cpp
|
||||
class MyModule : public ProtobufModule<meshtastic_MyMessage>
|
||||
class MyModule : public ProtobufModule<meshtastic_MyMessage>, private concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
MyModule();
|
||||
|
||||
protected:
|
||||
virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_MyMessage *msg) override;
|
||||
virtual int32_t runOnce() override;
|
||||
virtual meshtastic_MeshPacket *allocReply() override; // Generate response packets
|
||||
virtual int32_t runOnce() override; // Periodic task (returns next interval in ms)
|
||||
virtual bool alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_MyMessage *msg); // Modify in-flight
|
||||
virtual bool wantUIFrame(); // Request a UI display frame
|
||||
};
|
||||
```
|
||||
|
||||
Modules are registered in `src/modules/Modules.cpp` guarded by `MESHTASTIC_EXCLUDE_*` flags.
|
||||
|
||||
#### Observer/Observable Pattern
|
||||
|
||||
Event-driven communication between subsystems uses `src/Observer.h`:
|
||||
|
||||
```cpp
|
||||
// Observable emits events
|
||||
Observable<const meshtastic::Status *> newStatus;
|
||||
newStatus.notifyObservers(&status);
|
||||
|
||||
// Observer receives events via callback
|
||||
CallbackObserver<MyClass, const meshtastic::Status *> statusObserver =
|
||||
CallbackObserver<MyClass, const meshtastic::Status *>(this, &MyClass::handleStatusUpdate);
|
||||
```
|
||||
|
||||
#### Configuration Access
|
||||
|
||||
- `config.*` - Device configuration (LoRa, position, power, etc.)
|
||||
- `moduleConfig.*` - Module-specific configuration
|
||||
- `channels.*` - Channel configuration and management
|
||||
- `owner` - Device owner info
|
||||
- `myNodeInfo` - Local node info
|
||||
|
||||
#### Default Values
|
||||
|
||||
Use the `Default` class helpers in `src/mesh/Default.h`:
|
||||
|
||||
- `Default::getConfiguredOrDefaultMs(configured, default)` - Returns ms, using default if configured is 0
|
||||
- `Default::getConfiguredOrDefault(configured, default)` - Generic configured/default getter
|
||||
- `Default::getConfiguredOrMinimumValue(configured, min)` - Enforces minimum values
|
||||
- `Default::getConfiguredOrDefaultMsScaled(configured, default, numNodes)` - Scales based on network size
|
||||
|
||||
#### Thread Safety
|
||||
|
||||
- Use `concurrency::Lock` for mutex protection
|
||||
- Use `concurrency::Lock` and `concurrency::LockGuard` for mutex protection
|
||||
- Radio SPI access uses `SPILock`
|
||||
- Prefer `OSThread` for background tasks
|
||||
|
||||
### Hardware Detection
|
||||
|
||||
`src/detect/ScanI2C` automatically enumerates 80+ I2C device types at boot including displays, sensors, RTCs, keyboards, PMUs, and touch controllers. This drives automatic initialization of the correct drivers.
|
||||
|
||||
### Graphics/UI System
|
||||
|
||||
Multiple display driver families in `src/graphics/`:
|
||||
|
||||
- **OLED**: SSD1306, SH1106, ST7567
|
||||
- **TFT**: TFTDisplay (LovyanGFX-based)
|
||||
- **E-Ink**: EInkDisplay2, EInkDynamicDisplay, EInkParallelDisplay
|
||||
|
||||
**InkHUD** (`src/graphics/niche/InkHUD/`) is an event-driven e-ink UI framework:
|
||||
|
||||
- Applet-based architecture — modular display tiles
|
||||
- Read-only, static display optimized for minimal refreshes and low power
|
||||
- Configured per-variant via `nicheGraphics.h`
|
||||
- Separate PlatformIO config: `src/graphics/niche/InkHUD/PlatformioConfig.ini`
|
||||
|
||||
### Input System
|
||||
|
||||
`src/input/InputBroker` is the centralized input event dispatcher. Supports multiple input sources: buttons, keyboards (BBQ10, Cardputer, TCA8418), touch screens, rotary encoders, and matrix keyboards.
|
||||
|
||||
### Power Management
|
||||
|
||||
`src/PowerFSM.*` implements a finite state machine with states: `stateON`, `statePOWER`, `stateSERIAL`, `stateDARK`. Key events: `EVENT_PRESS`, `EVENT_WAKE_TIMER`, `EVENT_LOW_BATTERY`, `EVENT_RECEIVED_MSG`, `EVENT_SHUTDOWN`. Conditionally excluded with `MESHTASTIC_EXCLUDE_POWER_FSM` (falls back to `FakeFsm`).
|
||||
|
||||
### Motion Sensors
|
||||
|
||||
`src/motion/AccelerometerThread` provides background motion monitoring with automatic screen wake and double-tap button press detection. Supports 10+ accelerometer/gyroscope chips (BMA423, BMI270, MPU6050, LIS3DH, LSM6DS3, STK8XXX, QMA6100P, ICM20948, BMX160).
|
||||
|
||||
### Telemetry Sensor Library
|
||||
|
||||
`src/modules/Telemetry/Sensor/` contains 50+ I2C sensor drivers organized by category:
|
||||
|
||||
- **Power monitoring**: INA219/226/260/3221, MAX17048
|
||||
- **Environmental**: BME280/680, SCD4X (CO₂), SEN5X (particulate)
|
||||
- **Humidity/Temperature**: SHT3X/4X, AHT10, MCP9808, MLX90614
|
||||
- **Light**: BH1750, TSL2561/2591, VEML7700, LTR390UV, OPT3001
|
||||
- **Air quality**: PMSA003I, SFA30
|
||||
- **Specialized**: CGRadSens (radiation), NAU7802 (weight scale)
|
||||
|
||||
### API/Networking
|
||||
|
||||
`src/mesh/api/` provides a template-based `ServerAPI` for client communication over WiFi (`WiFiServerAPI`) and Ethernet (`ethServerAPI`). Default port: **4403**. HTTP server in `src/mesh/http/`. JSON serialization in `src/serialization/MeshPacketSerializer`.
|
||||
|
||||
### Hardware Variants
|
||||
|
||||
Each hardware variant has:
|
||||
|
||||
- `variant.h` - Pin definitions and hardware capabilities
|
||||
- `platformio.ini` - Build configuration
|
||||
- Optional: `pins_arduino.h`, `rfswitch.h`
|
||||
- Optional: `pins_arduino.h`, `rfswitch.h`, `nicheGraphics.h` (for InkHUD variants)
|
||||
|
||||
Key defines in variant.h:
|
||||
|
||||
```cpp
|
||||
#define USE_SX1262 // Radio chip selection
|
||||
#define HAS_GPS 1 // Hardware capabilities
|
||||
#define HAS_SCREEN 1 // Display present
|
||||
#define LORA_CS 36 // Pin assignments
|
||||
#define SX126X_DIO1 14 // Radio-specific pins
|
||||
```
|
||||
|
||||
### Protobuf Messages
|
||||
|
||||
- Defined in `protobufs/meshtastic/*.proto`
|
||||
- Generated code in `src/mesh/generated/`
|
||||
- Defined in `protobufs/meshtastic/*.proto` (~32 proto files)
|
||||
- Generated code in `src/mesh/generated/meshtastic/`
|
||||
- Regenerate with `bin/regen-protos.sh`
|
||||
- Message types prefixed with `meshtastic_`
|
||||
- Nanopb `.options` files control field sizes and encoding
|
||||
|
||||
### Conditional Compilation
|
||||
|
||||
```cpp
|
||||
#if !MESHTASTIC_EXCLUDE_GPS // Feature exclusion
|
||||
#if !MESHTASTIC_EXCLUDE_WIFI // Network feature exclusion
|
||||
#if !MESHTASTIC_EXCLUDE_BLUETOOTH // BLE exclusion
|
||||
#if !MESHTASTIC_EXCLUDE_POWER_FSM // Power FSM exclusion
|
||||
#ifdef ARCH_ESP32 // Architecture-specific
|
||||
#ifdef ARCH_NRF52 // Nordic platform
|
||||
#ifdef ARCH_RP2040 // Raspberry Pi Pico
|
||||
#ifdef ARCH_PORTDUINO // Linux native
|
||||
#if defined(USE_SX1262) // Radio-specific
|
||||
#ifdef HAS_SCREEN // Hardware capability
|
||||
#if USERPREFS_EVENT_MODE // User preferences
|
||||
@@ -192,7 +299,7 @@ Key defines in variant.h:
|
||||
Uses **PlatformIO** with custom scripts:
|
||||
|
||||
- `bin/platformio-pre.py` - Pre-build script
|
||||
- `bin/platformio-custom.py` - Custom build logic
|
||||
- `bin/platformio-custom.py` - Custom build logic, manifest generation
|
||||
|
||||
Build commands:
|
||||
|
||||
@@ -202,21 +309,38 @@ pio run -e tbeam -t upload # Build and upload
|
||||
pio run -e native # Build native/Linux version
|
||||
```
|
||||
|
||||
### Build Manifest
|
||||
|
||||
`bin/platformio-custom.py` emits a build manifest with metadata:
|
||||
|
||||
- `hasMui`, `hasInkHud` - UI capability flags (overridable via `custom_meshtastic_has_mui`, `custom_meshtastic_has_ink_hud`)
|
||||
- Architecture normalization (e.g., `esp32s3` → `esp32-s3` for API compatibility)
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Adding a New Module
|
||||
|
||||
1. Create `src/modules/MyModule.cpp` and `.h`
|
||||
2. Inherit from appropriate base class
|
||||
3. Register in `src/modules/Modules.cpp`
|
||||
4. Add protobuf messages if needed in `protobufs/`
|
||||
2. Inherit from appropriate base class (`MeshModule`, `SinglePortModule`, or `ProtobufModule<T>`)
|
||||
3. Mix in `concurrency::OSThread` if periodic work is needed
|
||||
4. Register in `src/modules/Modules.cpp` guarded by `#if !MESHTASTIC_EXCLUDE_MYMODULE`
|
||||
5. Add protobuf messages if needed in `protobufs/meshtastic/`
|
||||
6. Add test suite in `test/test_mymodule/` if applicable
|
||||
|
||||
### Adding a New Hardware Variant
|
||||
|
||||
1. Create directory under `variants/<arch>/<name>/`
|
||||
2. Add `variant.h` with pin definitions
|
||||
3. Add `platformio.ini` with build config
|
||||
4. Reference common configs with `extends`
|
||||
2. Add `variant.h` with pin definitions and hardware capability defines
|
||||
3. Add `platformio.ini` with build config — use `extends` to reference common base (e.g., `esp32s3_base`)
|
||||
4. Set `custom_meshtastic_support_level = 1` (PR builds) or `2` (merge builds)
|
||||
5. For e-ink displays, add `nicheGraphics.h` for InkHUD configuration
|
||||
|
||||
### Adding a New Telemetry Sensor
|
||||
|
||||
1. Create driver in `src/modules/Telemetry/Sensor/` following existing sensor pattern
|
||||
2. Register I2C address in `src/detect/ScanI2C` for auto-detection
|
||||
3. Integrate with the appropriate telemetry module (Environment, Health, Power, AirQuality)
|
||||
4. Add proto fields in `protobufs/meshtastic/telemetry.proto` if new data types are needed
|
||||
|
||||
### Modifying Configuration Defaults
|
||||
|
||||
@@ -305,9 +429,22 @@ Most workflows can be triggered manually via `workflow_dispatch` for testing.
|
||||
|
||||
## Testing
|
||||
|
||||
- Unit tests in `test/` directory
|
||||
- Run with `pio test -e native`
|
||||
- Use `bin/test-simulator.sh` for simulation testing
|
||||
Unit tests in `test/` directory with 12 test suites:
|
||||
|
||||
- `test_crypto/` - Cryptography
|
||||
- `test_mqtt/` - MQTT integration
|
||||
- `test_radio/` - Radio interface
|
||||
- `test_mesh_module/` - Module framework
|
||||
- `test_meshpacket_serializer/` - Packet serialization
|
||||
- `test_transmit_history/` - Retransmission tracking
|
||||
- `test_atak/` - ATAK integration
|
||||
- `test_default/` - Default configuration
|
||||
- `test_http_content_handler/` - HTTP handling
|
||||
- `test_serial/` - Serial communication
|
||||
|
||||
Run with: `pio test -e native`
|
||||
|
||||
Simulation testing: `bin/test-simulator.sh`
|
||||
|
||||
## Resources
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
# New Meshtastic Module
|
||||
|
||||
Guide for developing a new Meshtastic firmware module.
|
||||
|
||||
## Module Hierarchy
|
||||
|
||||
Choose the appropriate base class:
|
||||
|
||||
1. **`MeshModule`** — Raw base class. Override `wantPacket()` and `handleReceived()`. Returns `ProcessMessage::STOP` or `ProcessMessage::CONTINUE`.
|
||||
2. **`SinglePortModule`** — Handles a single `meshtastic_PortNum`. Constructor takes `(name, portNum)`. Simplified `wantPacket()` checking `decoded.portnum`. Use `allocDataPacket()` to create outgoing packets.
|
||||
3. **`ProtobufModule<T>`** — Template for protobuf-encoded modules. Constructor takes `(name, portNum, fields)`. Override `handleReceivedProtobuf()`. Use `allocDataProtobuf(payload)` to create outgoing packets.
|
||||
|
||||
Most modules also mix in `concurrency::OSThread` for periodic background tasks.
|
||||
|
||||
## Implementation Pattern
|
||||
|
||||
```cpp
|
||||
// src/modules/MyModule.h
|
||||
#pragma once
|
||||
#include "ProtobufModule.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
|
||||
class MyModule : public ProtobufModule<meshtastic_MyMessage>, private concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
MyModule();
|
||||
|
||||
protected:
|
||||
// Process incoming protobuf packet. Return true to stop further processing.
|
||||
virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_MyMessage *msg) override;
|
||||
|
||||
// Generate response packet (optional)
|
||||
virtual meshtastic_MeshPacket *allocReply() override;
|
||||
|
||||
// Periodic task — return next run interval in ms, or disable()
|
||||
virtual int32_t runOnce() override;
|
||||
|
||||
// Modify packet in-flight before delivery (optional)
|
||||
virtual bool alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_MyMessage *msg);
|
||||
|
||||
// Request a UI display frame (optional)
|
||||
virtual bool wantUIFrame();
|
||||
};
|
||||
```
|
||||
|
||||
## Registration
|
||||
|
||||
Register in `src/modules/Modules.cpp` inside `setupModules()`:
|
||||
|
||||
```cpp
|
||||
#if !MESHTASTIC_EXCLUDE_MYMODULE
|
||||
new MyModule();
|
||||
#endif
|
||||
```
|
||||
|
||||
If other code needs to reference the module instance:
|
||||
|
||||
```cpp
|
||||
#if !MESHTASTIC_EXCLUDE_MYMODULE
|
||||
myModule = new MyModule();
|
||||
#endif
|
||||
```
|
||||
|
||||
And declare the global in the header:
|
||||
|
||||
```cpp
|
||||
extern MyModule *myModule;
|
||||
```
|
||||
|
||||
Some modules also conditionally instantiate based on `moduleConfig`:
|
||||
|
||||
```cpp
|
||||
#if !MESHTASTIC_EXCLUDE_MYMODULE
|
||||
if (moduleConfig.has_my_module && moduleConfig.my_module.enabled) {
|
||||
new MyModule();
|
||||
}
|
||||
#endif
|
||||
```
|
||||
|
||||
## Conditional Compilation
|
||||
|
||||
Add a `MESHTASTIC_EXCLUDE_MYMODULE` guard. This allows the module to be excluded from constrained builds. The flag name must follow the pattern: `MESHTASTIC_EXCLUDE_` + uppercase module name.
|
||||
|
||||
## Protobuf Messages (if needed)
|
||||
|
||||
1. Define messages in `protobufs/meshtastic/` (e.g., `mymodule.proto`)
|
||||
2. Add a `.options` file for nanopb field size constraints
|
||||
3. Regenerate with `bin/regen-protos.sh`
|
||||
4. Generated code appears in `src/mesh/generated/meshtastic/`
|
||||
5. Assign a `meshtastic_PortNum` if the module uses a new port number
|
||||
|
||||
## Timing and Defaults
|
||||
|
||||
Use `Default` class helpers for configurable intervals:
|
||||
|
||||
```cpp
|
||||
int32_t MyModule::runOnce()
|
||||
{
|
||||
uint32_t interval = Default::getConfiguredOrDefaultMs(moduleConfig.my_module.update_interval,
|
||||
default_my_module_interval);
|
||||
// ... do work ...
|
||||
return interval;
|
||||
}
|
||||
```
|
||||
|
||||
On public/default channels, enforce minimums with `Default::getConfiguredOrMinimumValue()`.
|
||||
|
||||
## Observer Pattern
|
||||
|
||||
Subscribe to system events:
|
||||
|
||||
```cpp
|
||||
CallbackObserver<MyModule, const meshtastic::Status *> statusObserver =
|
||||
CallbackObserver<MyModule, const meshtastic::Status *>(this, &MyModule::handleStatusUpdate);
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Add test suite in `test/test_mymodule/`:
|
||||
|
||||
```
|
||||
test/
|
||||
└── test_mymodule/
|
||||
└── test_main.cpp
|
||||
```
|
||||
|
||||
Run with: `pio test -e native`
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Header and implementation files in `src/modules/`
|
||||
- [ ] Inherit from appropriate base class (MeshModule / SinglePortModule / ProtobufModule)
|
||||
- [ ] Mix in OSThread if periodic work is needed
|
||||
- [ ] Register in `src/modules/Modules.cpp` with `MESHTASTIC_EXCLUDE_` guard
|
||||
- [ ] Add protobuf definitions if needed (`protobufs/meshtastic/`)
|
||||
- [ ] Use `Default::getConfiguredOrDefaultMs()` for timing
|
||||
- [ ] Respect bandwidth limits on public channels
|
||||
- [ ] Add test suite in `test/`
|
||||
@@ -0,0 +1,149 @@
|
||||
# New Telemetry Sensor
|
||||
|
||||
Guide for adding a new I2C telemetry sensor driver to Meshtastic firmware.
|
||||
|
||||
## Overview
|
||||
|
||||
Telemetry sensors live in `src/modules/Telemetry/Sensor/`. There are 50+ existing drivers organized by measurement type. Each sensor integrates with one of the telemetry modules:
|
||||
|
||||
- **EnvironmentTelemetryModule** — Temperature, humidity, pressure, gas, light
|
||||
- **AirQualityTelemetryModule** — Particulate matter, VOCs
|
||||
- **PowerTelemetryModule** — Voltage, current, power monitoring
|
||||
- **HealthTelemetryModule** — Heart rate, SpO2, body temperature
|
||||
|
||||
## Sensor Driver Pattern
|
||||
|
||||
Each sensor has a `.h` and `.cpp` file pair following this pattern:
|
||||
|
||||
```cpp
|
||||
// src/modules/Telemetry/Sensor/MySensor.h
|
||||
#pragma once
|
||||
#include "TelemetrySensor.h"
|
||||
#include <MySensorLibrary.h> // Arduino/PlatformIO library
|
||||
|
||||
class MySensor : virtual public TelemetrySensor
|
||||
{
|
||||
private:
|
||||
MySensorLibrary sensor;
|
||||
|
||||
public:
|
||||
MySensor() : TelemetrySensor(meshtastic_TelemetrySensorType_MY_SENSOR, "MySensor") {}
|
||||
|
||||
// Initialize sensor hardware. Return true on success.
|
||||
virtual void setup() override;
|
||||
|
||||
// Read sensor data into the telemetry protobuf. Return true on success.
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
};
|
||||
```
|
||||
|
||||
```cpp
|
||||
// src/modules/Telemetry/Sensor/MySensor.cpp
|
||||
#include "MySensor.h"
|
||||
#include "TelemetrySensor.h"
|
||||
|
||||
void MySensor::setup()
|
||||
{
|
||||
sensor.begin();
|
||||
// Configure sensor parameters...
|
||||
}
|
||||
|
||||
bool MySensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
// Read from hardware
|
||||
float value = sensor.readValue();
|
||||
|
||||
// Populate the appropriate protobuf variant
|
||||
measurement->variant.environment_metrics.temperature = value;
|
||||
// ... other fields ...
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
## I2C Address Registration
|
||||
|
||||
Register the sensor's I2C address(es) in `src/detect/ScanI2C` so it's auto-detected at boot:
|
||||
|
||||
1. Add a `DeviceType` enum entry in `src/detect/ScanI2C.h`
|
||||
2. Add the I2C address mapping in `src/detect/ScanI2CTwoWire.cpp`
|
||||
|
||||
The scan runs at boot and populates a device map that telemetry modules use to decide which sensors to initialize.
|
||||
|
||||
## Protobuf Fields
|
||||
|
||||
If the sensor provides data not covered by existing telemetry fields:
|
||||
|
||||
1. Add fields to the appropriate message in `protobufs/meshtastic/telemetry.proto`:
|
||||
- `EnvironmentMetrics` — Environmental measurements
|
||||
- `AirQualityMetrics` — Air quality data
|
||||
- `PowerMetrics` — Power/energy data
|
||||
- `HealthMetrics` — Health/biometric data
|
||||
2. Add a `.options` constraint if needed (field sizes for nanopb)
|
||||
3. Regenerate: `bin/regen-protos.sh`
|
||||
|
||||
## Sensor Type Enum
|
||||
|
||||
Add the sensor to `meshtastic_TelemetrySensorType` enum in `protobufs/meshtastic/telemetry.proto`:
|
||||
|
||||
```protobuf
|
||||
enum TelemetrySensorType {
|
||||
// ... existing entries ...
|
||||
MY_SENSOR = XX;
|
||||
}
|
||||
```
|
||||
|
||||
## Integration with Telemetry Module
|
||||
|
||||
Wire the sensor into the appropriate telemetry module. For environment sensors, this is typically in `src/modules/Telemetry/EnvironmentTelemetry.cpp`:
|
||||
|
||||
1. Include the sensor header
|
||||
2. Add initialization in `setupSensor()` guarded by detection results
|
||||
3. Call `getMetrics()` in the measurement collection path
|
||||
|
||||
Example pattern from existing sensors:
|
||||
|
||||
```cpp
|
||||
#include "Sensor/MySensor.h"
|
||||
|
||||
MySensor mySensor;
|
||||
|
||||
// In setup:
|
||||
if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MY_SENSOR].first > 0) {
|
||||
mySensor.setup();
|
||||
}
|
||||
|
||||
// In measurement collection:
|
||||
if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MY_SENSOR].first > 0) {
|
||||
mySensor.getMetrics(&measurement);
|
||||
}
|
||||
```
|
||||
|
||||
## Library Dependencies
|
||||
|
||||
If the sensor needs an external library, add it to the `lib_deps` in the relevant base platformio.ini configs:
|
||||
|
||||
```ini
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
mysensorlibrary@^1.0.0
|
||||
```
|
||||
|
||||
Or use a conditional dependency if it's platform-specific.
|
||||
|
||||
## Unit Conversions
|
||||
|
||||
If the sensor reports values in non-standard units, use `src/modules/Telemetry/UnitConversions.h` for conversion helpers (e.g., Celsius ↔ Fahrenheit, hPa ↔ inHg).
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Create `src/modules/Telemetry/Sensor/MySensor.h` and `.cpp`
|
||||
- [ ] Inherit from `TelemetrySensor` base class
|
||||
- [ ] Implement `setup()` and `getMetrics()` methods
|
||||
- [ ] Add `meshtastic_TelemetrySensorType` enum entry in `telemetry.proto`
|
||||
- [ ] Add I2C address to `src/detect/ScanI2C` for auto-detection
|
||||
- [ ] Add protobuf fields in `telemetry.proto` if new data types needed
|
||||
- [ ] Regenerate protos: `bin/regen-protos.sh`
|
||||
- [ ] Wire into the appropriate telemetry module (Environment/AirQuality/Power/Health)
|
||||
- [ ] Add library dependency if external library required
|
||||
- [ ] Test on hardware or native build
|
||||
@@ -0,0 +1,178 @@
|
||||
# New Hardware Variant
|
||||
|
||||
Guide for adding a new Meshtastic hardware variant to the firmware.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
Create under `variants/<arch>/<name>/`:
|
||||
|
||||
```
|
||||
variants/
|
||||
├── esp32/ # ESP32
|
||||
├── esp32s3/ # ESP32-S3
|
||||
├── esp32c3/ # ESP32-C3
|
||||
├── esp32c6/ # ESP32-C6
|
||||
├── nrf52840/ # nRF52840
|
||||
├── rp2040/ # RP2040/RP2350
|
||||
├── stm32/ # STM32WL
|
||||
└── native/ # Linux/Portduino
|
||||
```
|
||||
|
||||
Each variant needs at minimum:
|
||||
|
||||
- `variant.h` — Pin definitions and hardware capabilities
|
||||
- `platformio.ini` — Build configuration
|
||||
|
||||
Optional files:
|
||||
|
||||
- `pins_arduino.h` — Arduino pin mapping overrides
|
||||
- `rfswitch.h` — RF switch control for multi-band radios
|
||||
- `nicheGraphics.h` — InkHUD e-ink configuration
|
||||
|
||||
## variant.h Template
|
||||
|
||||
```cpp
|
||||
// Pin definitions
|
||||
#define I2C_SDA 21
|
||||
#define I2C_SCL 22
|
||||
|
||||
// LoRa radio
|
||||
#define USE_SX1262 // Radio chip: USE_SX1262, USE_SX1268, USE_SX1280, USE_RF95, USE_LLCC68, USE_LR1110, USE_LR1120, USE_LR1121
|
||||
#define LORA_CS 18
|
||||
#define LORA_SCK 5
|
||||
#define LORA_MOSI 27
|
||||
#define LORA_MISO 19
|
||||
#define LORA_DIO1 33 // SX126x: DIO1, SX128x: DIO1, RF95: IRQ
|
||||
#define LORA_RESET 23
|
||||
#define LORA_BUSY 32 // SX126x/SX128x only
|
||||
#define SX126X_DIO2_AS_RF_SWITCH // Common for SX1262 boards
|
||||
|
||||
// GPS
|
||||
#define HAS_GPS 1
|
||||
#define GPS_RX_PIN 34
|
||||
#define GPS_TX_PIN 12
|
||||
// #define PIN_GPS_EN 47 // Optional GPS enable pin
|
||||
// #define GPS_BAUDRATE 9600 // Override default 9600
|
||||
|
||||
// Display
|
||||
#define HAS_SCREEN 1
|
||||
// #define USE_SSD1306 // OLED type
|
||||
// #define USE_SH1106 // Alternative OLED
|
||||
// #define USE_ST7789 // TFT type
|
||||
// #define SCREEN_WIDTH 128
|
||||
// #define SCREEN_HEIGHT 64
|
||||
|
||||
// LEDs
|
||||
#define LED_PIN 2 // Status LED (optional)
|
||||
// #define HAS_NEOPIXEL 1 // WS2812 support
|
||||
|
||||
// Buttons
|
||||
#define BUTTON_PIN 38
|
||||
// #define BUTTON_PIN_ALT 0 // Secondary button
|
||||
|
||||
// Power management
|
||||
// #define HAS_AXP192 1 // AXP192 PMU (T-Beam v1.0)
|
||||
// #define HAS_AXP2101 1 // AXP2101 PMU (T-Beam v1.2+)
|
||||
// #define BATTERY_PIN 35 // ADC battery voltage pin
|
||||
// #define ADC_MULTIPLIER 2.0 // Voltage divider ratio
|
||||
|
||||
// Optional I2C devices
|
||||
// #define HAS_RTC 1 // Real-time clock
|
||||
// #define HAS_TELEMETRY 1 // Enable telemetry sensor support
|
||||
// #define HAS_SENSOR 1 // I2C sensors present
|
||||
```
|
||||
|
||||
## platformio.ini Template
|
||||
|
||||
```ini
|
||||
[env:my_variant]
|
||||
extends = esp32s3_base ; Use architecture-specific base
|
||||
board = esp32-s3-devkitc-1 ; PlatformIO board definition (or custom in boards/)
|
||||
board_level = extra ; Build level: extra, or omit for default
|
||||
custom_meshtastic_support_level = 1 ; 1 = PR builds, 2 = merge builds only
|
||||
|
||||
build_flags =
|
||||
${esp32s3_base.build_flags}
|
||||
-D MY_VARIANT_SPECIFIC_FLAG=1
|
||||
-I variants/esp32s3/my_variant ; Include path for variant.h
|
||||
|
||||
upload_speed = 921600
|
||||
```
|
||||
|
||||
### Common Base Configs
|
||||
|
||||
- `esp32_base` / `esp32-common.ini` — ESP32
|
||||
- `esp32s3_base` — ESP32-S3
|
||||
- `esp32c3_base` — ESP32-C3
|
||||
- `esp32c6_base` — ESP32-C6
|
||||
- `nrf52840_base` / `nrf52.ini` — nRF52840
|
||||
- `rp2040_base` — RP2040/RP2350
|
||||
|
||||
### Support Levels
|
||||
|
||||
- `custom_meshtastic_support_level = 1` — Built on every PR (actively supported)
|
||||
- `custom_meshtastic_support_level = 2` — Built only on merge to main branches
|
||||
- `board_level = extra` — Only built on full releases
|
||||
|
||||
## Build Manifest Metadata
|
||||
|
||||
`bin/platformio-custom.py` emits UI capability flags in the build manifest:
|
||||
|
||||
- `custom_meshtastic_has_mui = true/false` — Override MUI detection
|
||||
- `custom_meshtastic_has_ink_hud = true/false` — Override InkHUD detection
|
||||
- Architecture names are normalized (e.g., `esp32s3` → `esp32-s3`)
|
||||
|
||||
## InkHUD E-Ink Variants
|
||||
|
||||
For e-ink display variants using the InkHUD framework, add `nicheGraphics.h`:
|
||||
|
||||
```cpp
|
||||
// nicheGraphics.h — InkHUD configuration for this variant
|
||||
#define INKHUD // Enable InkHUD
|
||||
// Configure display, applets, and refresh behavior per device
|
||||
```
|
||||
|
||||
InkHUD has its own PlatformIO config: `src/graphics/niche/InkHUD/PlatformioConfig.ini`
|
||||
|
||||
## I2C Device Detection
|
||||
|
||||
If the variant has I2C devices, ensure `src/detect/ScanI2C` will detect them. The auto-detection system handles 80+ device types including displays, sensors, RTCs, keyboards, PMUs, and touch controllers at boot.
|
||||
|
||||
## Custom Board Definitions
|
||||
|
||||
If the PlatformIO board doesn't exist, create a custom board JSON in `boards/`:
|
||||
|
||||
```json
|
||||
{
|
||||
"build": {
|
||||
"arduino": { "ldscript": "esp32s3_out.ld" },
|
||||
"core": "esp32",
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"flash_mode": "qio",
|
||||
"mcu": "esp32s3",
|
||||
"variant": "esp32s3"
|
||||
},
|
||||
"connectivity": ["wifi", "bluetooth"],
|
||||
"frameworks": ["arduino", "espidf"],
|
||||
"name": "My Custom Board",
|
||||
"upload": {
|
||||
"flash_size": "8MB",
|
||||
"maximum_ram_size": 327680,
|
||||
"maximum_size": 8388608
|
||||
},
|
||||
"url": "https://example.com",
|
||||
"vendor": "MyVendor"
|
||||
}
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Create `variants/<arch>/<name>/variant.h` with pin definitions
|
||||
- [ ] Create `variants/<arch>/<name>/platformio.ini` extending correct base
|
||||
- [ ] Set `custom_meshtastic_support_level` (1 or 2)
|
||||
- [ ] Verify radio chip define matches hardware (`USE_SX1262`, etc.)
|
||||
- [ ] Set hardware capability flags (`HAS_GPS`, `HAS_SCREEN`, etc.)
|
||||
- [ ] Add custom board JSON in `boards/` if needed
|
||||
- [ ] Test build: `pio run -e my_variant`
|
||||
- [ ] For e-ink: add `nicheGraphics.h` with InkHUD config
|
||||
@@ -56,6 +56,25 @@ jobs:
|
||||
echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
|
||||
id: version
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
with:
|
||||
cache-image: true
|
||||
|
||||
- name: Docker setup
|
||||
uses: docker/setup-buildx-action@v4
|
||||
with:
|
||||
# Add Google/Amazon DockerHub mirrors to buildkit config
|
||||
# https://docs.docker.com/build/ci/github-actions/configure-builder/#registry-mirror
|
||||
buildkitd-config-inline: |
|
||||
[registry."docker.io"]
|
||||
mirrors = ["mirror.gcr.io", "public.ecr.aws"]
|
||||
|
||||
- name: Sanitize platform string
|
||||
id: sanitize_platform
|
||||
# Replace slashes with underscores
|
||||
run: echo "cleaned_platform=${{ inputs.platform }}" | sed 's/\//_/g' >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Docker login
|
||||
if: ${{ inputs.push }}
|
||||
uses: docker/login-action@v4
|
||||
@@ -63,17 +82,6 @@ jobs:
|
||||
username: meshtastic
|
||||
password: ${{ secrets.DOCKER_FIRMWARE_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Docker setup
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Sanitize platform string
|
||||
id: sanitize_platform
|
||||
# Replace slashes with underscores
|
||||
run: echo "cleaned_platform=${{ inputs.platform }}" | sed 's/\//_/g' >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Docker tag
|
||||
id: meta
|
||||
uses: docker/metadata-action@v6
|
||||
@@ -95,3 +103,6 @@ jobs:
|
||||
platforms: ${{ inputs.platform }}
|
||||
build-args: |
|
||||
PIO_ENV=${{ inputs.pio_env }}
|
||||
# Cache image layers in GitHub Actions cache to speed up subsequent builds.
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
@@ -301,15 +301,17 @@ jobs:
|
||||
id: release_notes
|
||||
run: |
|
||||
chmod +x ./bin/generate_release_notes.py
|
||||
NOTES=$(./bin/generate_release_notes.py ${{ needs.version.outputs.long }})
|
||||
NOTES=$(./bin/generate_release_notes.py ${{ needs.version.outputs.long }} --compare-ref HEAD 2>release_notes.log)
|
||||
echo "notes<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$NOTES" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
echo "### Release note range" >> $GITHUB_STEP_SUMMARY
|
||||
cat release_notes.log >> $GITHUB_STEP_SUMMARY
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
id: create_release
|
||||
with:
|
||||
draft: true
|
||||
@@ -466,7 +468,7 @@ jobs:
|
||||
- name: Generate release notes
|
||||
run: |
|
||||
chmod +x ./bin/generate_release_notes.py
|
||||
./bin/generate_release_notes.py ${{ needs.version.outputs.long }} > ./publish/release_notes.md
|
||||
./bin/generate_release_notes.py ${{ needs.version.outputs.long }} --compare-ref HEAD > ./publish/release_notes.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
- name: Apply quality label if needed
|
||||
if: steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok'
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
QUALITY_LABEL: ${{ steps.quality.outputs.response }}
|
||||
with:
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
- name: Determine if completeness check should be skipped
|
||||
if: steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == ''
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
id: check-skip
|
||||
with:
|
||||
script: |
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
|
||||
- name: Process analysis result
|
||||
if: (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') && steps.check-skip.outputs.should_skip != 'true' && steps.analysis.outputs.response != ''
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
id: process
|
||||
env:
|
||||
AI_RESPONSE: ${{ steps.analysis.outputs.response }}
|
||||
@@ -174,7 +174,7 @@ jobs:
|
||||
|
||||
- name: Apply triage label
|
||||
if: steps.process.outputs.label != '' && steps.process.outputs.label != 'none'
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
LABEL_NAME: ${{ steps.process.outputs.label }}
|
||||
with:
|
||||
@@ -200,7 +200,7 @@ jobs:
|
||||
|
||||
- name: Comment on issue
|
||||
if: steps.process.outputs.should_comment == 'true'
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
COMMENT_BODY: ${{ steps.process.outputs.comment_body }}
|
||||
with:
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
# Step 1: Check if PR already has automation/type labels (skip if so)
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
- name: Check existing labels
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
id: check-labels
|
||||
with:
|
||||
script: |
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
- name: Apply quality label if needed
|
||||
if: steps.check-labels.outputs.skip_all != 'true' && steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok'
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
id: quality-label
|
||||
env:
|
||||
QUALITY_LABEL: ${{ steps.quality.outputs.response }}
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
|
||||
- name: Apply type label
|
||||
if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && steps.classify.outputs.response != ''
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
TYPE_LABEL: ${{ steps.classify.outputs.response }}
|
||||
with:
|
||||
|
||||
@@ -5,6 +5,8 @@ on:
|
||||
secrets:
|
||||
PPA_GPG_PRIVATE_KEY:
|
||||
required: true
|
||||
PPA_SFTP_PRIVATE_KEY:
|
||||
required: true
|
||||
inputs:
|
||||
ppa_repo:
|
||||
description: Meshtastic PPA to target
|
||||
@@ -27,6 +29,7 @@ jobs:
|
||||
build_location: ppa
|
||||
|
||||
package-ppa:
|
||||
if: ${{ github.event_name != 'pull_request_target' && github.event_name != 'pull_request' }}
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-debian-src
|
||||
steps:
|
||||
@@ -40,7 +43,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update -y --fix-missing
|
||||
sudo apt-get install -y dput
|
||||
sudo apt-get install -y dput openssh-client
|
||||
|
||||
- name: Import GPG key
|
||||
uses: crazy-max/ghaction-import-gpg@v7
|
||||
@@ -65,8 +68,42 @@ jobs:
|
||||
- name: Display structure of downloaded files
|
||||
run: ls -lah
|
||||
|
||||
- name: Publish with dput
|
||||
if: ${{ github.event_name != 'pull_request_target' && github.event_name != 'pull_request' }}
|
||||
timeout-minutes: 15 # dput is terrible, sometimes runs 'forever'
|
||||
- name: Trust Launchpad's SSH key
|
||||
run: |
|
||||
dput ${{ inputs.ppa_repo }} meshtasticd_${{ steps.version.outputs.deb }}~${{ inputs.series }}_source.changes
|
||||
mkdir -p ~/.ssh
|
||||
ssh-keyscan -H ppa.launchpad.net >> ~/.ssh/known_hosts
|
||||
|
||||
- name: Setup dput config
|
||||
env:
|
||||
ppa_login: meshtasticorg
|
||||
run: |
|
||||
sudo tee /etc/meshtastic-dput.cf >/dev/null <<EOF
|
||||
[ppa]
|
||||
fqdn = ppa.launchpad.net
|
||||
method = ftp
|
||||
incoming = ~%(ppa)s
|
||||
login = anonymous
|
||||
|
||||
[ssh-ppa]
|
||||
fqdn = ppa.launchpad.net
|
||||
method = sftp
|
||||
incoming = ~%(ssh-ppa)s
|
||||
login = ${ppa_login}
|
||||
EOF
|
||||
|
||||
- name: Import SSH key
|
||||
uses: webfactory/ssh-agent@v0.10.0
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.PPA_SFTP_PRIVATE_KEY }}
|
||||
id: ssh
|
||||
|
||||
- name: Publish with dput (sftp)
|
||||
timeout-minutes: 30 # dput is terrible, sometimes runs 'forever'
|
||||
env:
|
||||
up_ppa_repo: ${{ inputs.ppa_repo }}
|
||||
up_series: ${{ inputs.series }}
|
||||
up_version: ${{ steps.version.outputs.deb }}
|
||||
run: >
|
||||
dput -c /etc/meshtastic-dput.cf
|
||||
ssh-${up_ppa_repo}
|
||||
meshtasticd_${up_version}~${up_series}_source.changes
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check for PR labels
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const labels = context.payload.pull_request.labels.map(label => label.name);
|
||||
|
||||
@@ -177,7 +177,7 @@ jobs:
|
||||
|
||||
- name: Comment test results on PR
|
||||
if: github.event_name == 'pull_request' && needs.native-tests.result != 'skipped'
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
node-version: 24
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v5
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: latest
|
||||
|
||||
|
||||
+3
-3
@@ -8,10 +8,10 @@ plugins:
|
||||
uri: https://github.com/trunk-io/plugins
|
||||
lint:
|
||||
enabled:
|
||||
- checkov@3.2.513
|
||||
- renovate@43.104.6
|
||||
- checkov@3.2.517
|
||||
- renovate@43.110.9
|
||||
- prettier@3.8.1
|
||||
- trufflehog@3.94.2
|
||||
- trufflehog@3.94.3
|
||||
- yamllint@1.38.0
|
||||
- bandit@1.9.4
|
||||
- trivy@0.69.3
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ ENV TZ=Etc/UTC
|
||||
ENV PIP_ROOT_USER_ACTION=ignore
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||
curl wget g++ zip git ca-certificates pkg-config \
|
||||
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
|
||||
libgpiod-dev libyaml-cpp-dev libjsoncpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
|
||||
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
|
||||
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
||||
@@ -51,7 +51,7 @@ ENV TZ=Etc/UTC
|
||||
USER root
|
||||
|
||||
RUN apt-get update && apt-get --no-install-recommends -y install \
|
||||
libc-bin libc6 libgpiod3 libyaml-cpp0.8 libi2c0 libuv1t64 libusb-1.0-0-dev \
|
||||
libc-bin libc6 libgpiod3 libyaml-cpp0.8 libjsoncpp26 libi2c0 libuv1t64 libusb-1.0-0-dev \
|
||||
liborcania2.3 libulfius2.7t64 libssl3t64 \
|
||||
libx11-6 libinput10 libxkbcommon-x11-0 libsdl2-2.0-0 \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ ENV PIP_ROOT_USER_ACTION=ignore
|
||||
# hadolint ignore=DL3008
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||
g++ git ca-certificates pkg-config \
|
||||
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
|
||||
libgpiod-dev libyaml-cpp-dev libjsoncpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
|
||||
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
|
||||
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ ENV PIP_ROOT_USER_ACTION=ignore
|
||||
|
||||
RUN apk --no-cache add \
|
||||
bash g++ libstdc++-dev linux-headers zip git ca-certificates libbsd-dev \
|
||||
libgpiod-dev yaml-cpp-dev bluez-dev \
|
||||
libgpiod-dev yaml-cpp-dev jsoncpp-dev bluez-dev \
|
||||
libusb-dev i2c-tools-dev libuv-dev openssl-dev pkgconf argp-standalone \
|
||||
libx11-dev libinput-dev libxkbcommon-dev sqlite-dev sdl2-dev \
|
||||
&& rm -rf /var/cache/apk/* \
|
||||
@@ -41,7 +41,7 @@ LABEL org.opencontainers.image.title="Meshtastic" \
|
||||
USER root
|
||||
|
||||
RUN apk --no-cache add \
|
||||
shadow libstdc++ libbsd libgpiod yaml-cpp libusb \
|
||||
shadow libstdc++ libbsd libgpiod yaml-cpp jsoncpp libusb \
|
||||
i2c-tools libuv libx11 libinput libxkbcommon sdl2 \
|
||||
&& rm -rf /var/cache/apk/* \
|
||||
&& mkdir -p /var/lib/meshtasticd \
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# MeshAdv-Pi E22-900M30S
|
||||
# https://github.com/chrismyers2000/MeshAdv-Pi-Hat
|
||||
Meta:
|
||||
name: MeshAdv-Pi E22-900M30S
|
||||
support: community
|
||||
compatible:
|
||||
- ebyte-ecb41-pge # Armbian
|
||||
|
||||
Lora:
|
||||
Module: sx1262
|
||||
CS: # GPIO0_A1 (physical 40)
|
||||
pin: 1
|
||||
gpiochip: 0
|
||||
line: 1
|
||||
IRQ: # GPIO0_A3 (physical 36)
|
||||
pin: 3
|
||||
gpiochip: 0
|
||||
line: 3
|
||||
Busy: # GPIO0_A0 (physical 38)
|
||||
pin: 0
|
||||
gpiochip: 0
|
||||
line: 0
|
||||
Reset: # GPIO0_B4 (physical 12)
|
||||
pin: 12
|
||||
gpiochip: 0
|
||||
line: 12
|
||||
TXen: # GPIO1_D1 (physical 33)
|
||||
pin: 57
|
||||
gpiochip: 1
|
||||
line: 25
|
||||
RXen: # GPIO1_B3 (physical 32)
|
||||
pin: 43
|
||||
gpiochip: 1
|
||||
line: 11
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
# Only for E22-900M33S:
|
||||
# Limit the output power to 8 dBm
|
||||
# SX126X_MAX_POWER: 8
|
||||
spidev: spidev0.0
|
||||
@@ -0,0 +1,33 @@
|
||||
# MeshAdv Mini E22-900M22S
|
||||
# https://github.com/chrismyers2000/MeshAdv-Mini
|
||||
Meta:
|
||||
name: MeshAdv Mini E22-900M22S
|
||||
support: community
|
||||
compatible:
|
||||
- ebyte-ecb41-pge # Armbian
|
||||
|
||||
Lora:
|
||||
Module: sx1262 # Ebyte E22-900M22S
|
||||
CS: # GPIO0_B6 (physical 24, SPI1_CSN0)
|
||||
pin: 14
|
||||
gpiochip: 0
|
||||
line: 14
|
||||
IRQ: # GPIO0_A3 (physical 36)
|
||||
pin: 3
|
||||
gpiochip: 0
|
||||
line: 3
|
||||
Busy: # GPIO0_A0 (physical 38)
|
||||
pin: 0
|
||||
gpiochip: 0
|
||||
line: 0
|
||||
Reset: # GPIO1_C3 (physical 18)
|
||||
pin: 51
|
||||
gpiochip: 1
|
||||
line: 19
|
||||
RXen: # GPIO1_B3 (physical 32)
|
||||
pin: 43
|
||||
gpiochip: 1
|
||||
line: 11
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
spidev: spidev0.0
|
||||
@@ -0,0 +1,38 @@
|
||||
Meta:
|
||||
name: RAK6421 + RAK13300 Slot 1
|
||||
support: community # Promote when tested
|
||||
compatible:
|
||||
- ebyte-ecb41-pge # Armbian
|
||||
|
||||
Lora:
|
||||
Module: sx1262
|
||||
IRQ: # GPIO0_A5 (IO6, physical 15)
|
||||
pin: 5
|
||||
gpiochip: 0
|
||||
line: 5
|
||||
Reset: # GPIO0_A3 (IO4, physical 36)
|
||||
pin: 3
|
||||
gpiochip: 0
|
||||
line: 3
|
||||
Busy: # GPIO1_C3 (IO5, physical 18)
|
||||
pin: 51
|
||||
gpiochip: 1
|
||||
line: 19
|
||||
# Ant_sw: # GPIO1_C2 (IO3, physical 16)
|
||||
# pin: 50
|
||||
# gpiochip: 1
|
||||
# line: 18
|
||||
Enable_Pins:
|
||||
- pin: 43 # GPIO1_B3 (physical 32)
|
||||
gpiochip: 1
|
||||
line: 11
|
||||
- pin: 57 # GPIO1_D1 (physical 33)
|
||||
gpiochip: 1
|
||||
line: 25
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: spidev0.0
|
||||
# CS: # GPIO0_B6 (SPI1_CSN0, physical 24)
|
||||
# pin: 14
|
||||
# gpiochip: 0
|
||||
# line: 14
|
||||
@@ -0,0 +1,36 @@
|
||||
Meta:
|
||||
name: RAK6421 + RAK13300 Slot 2
|
||||
support: community # Promote when tested
|
||||
compatible:
|
||||
- ebyte-ecb41-pge # Armbian
|
||||
|
||||
Lora:
|
||||
Module: sx1262
|
||||
IRQ: # GPIO0_B4 (IO6, physical 12)
|
||||
pin: 12
|
||||
gpiochip: 0
|
||||
line: 12
|
||||
Reset: # GPIO1_C3 (IO4, physical 18)
|
||||
pin: 51
|
||||
gpiochip: 1
|
||||
line: 19
|
||||
Busy: # GPIO0_B3 (IO5, physical 35)
|
||||
pin: 11
|
||||
gpiochip: 0
|
||||
line: 11
|
||||
# Ant_sw: # GPIO1_C2 (IO3, physical 16)
|
||||
# pin: 50
|
||||
# gpiochip: 1
|
||||
# line: 18
|
||||
Enable_Pins:
|
||||
- pin: 2 # GPIO0_A2 (physical 37)
|
||||
gpiochip: 0
|
||||
line: 2
|
||||
- pin: 50 # GPIO1_C2 (physical 16)
|
||||
gpiochip: 1
|
||||
line: 18
|
||||
spidev: spidev0.1
|
||||
# CS: # GPIO0_A7 (SPI1_CSN1, physical 26)
|
||||
# pin: 7
|
||||
# gpiochip: 0
|
||||
# line: 7
|
||||
@@ -0,0 +1,39 @@
|
||||
Meta:
|
||||
name: RAK6421 + RAK13302 Slot 1
|
||||
support: community # Promote when tested
|
||||
compatible:
|
||||
- ebyte-ecb41-pge # Armbian
|
||||
|
||||
Lora:
|
||||
Module: sx1262
|
||||
IRQ: # GPIO0_A5 (IO6, physical 15)
|
||||
pin: 5
|
||||
gpiochip: 0
|
||||
line: 5
|
||||
Reset: # GPIO0_A3 (IO4, physical 36)
|
||||
pin: 3
|
||||
gpiochip: 0
|
||||
line: 3
|
||||
Busy: # GPIO1_C3 (IO5, physical 18)
|
||||
pin: 51
|
||||
gpiochip: 1
|
||||
line: 19
|
||||
# Ant_sw: # GPIO1_C2 (IO3, physical 16)
|
||||
# pin: 50
|
||||
# gpiochip: 1
|
||||
# line: 18
|
||||
Enable_Pins:
|
||||
- pin: 43 # GPIO1_B3 (physical 32)
|
||||
gpiochip: 1
|
||||
line: 11
|
||||
- pin: 57 # GPIO1_D1 (physical 33)
|
||||
gpiochip: 1
|
||||
line: 25
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: spidev0.0
|
||||
# CS: # GPIO0_B6 (SPI1_CSN0, physical 24)
|
||||
# pin: 14
|
||||
# gpiochip: 0
|
||||
# line: 14
|
||||
TX_GAIN_LORA: [9, 9, 10, 11, 9, 8, 9, 10, 10, 10, 11, 12, 12, 12, 12, 12, 12, 12, 12, 10, 9, 8]
|
||||
@@ -0,0 +1,37 @@
|
||||
Meta:
|
||||
name: RAK6421 + RAK13302 Slot 2
|
||||
support: community # Promote when tested
|
||||
compatible:
|
||||
- ebyte-ecb41-pge # Armbian
|
||||
|
||||
Lora:
|
||||
Module: sx1262
|
||||
IRQ: # GPIO0_B4 (IO6, physical 12)
|
||||
pin: 12
|
||||
gpiochip: 0
|
||||
line: 12
|
||||
Reset: # GPIO1_C3 (IO4, physical 18)
|
||||
pin: 51
|
||||
gpiochip: 1
|
||||
line: 19
|
||||
Busy: # GPIO0_B3 (IO5, physical 35)
|
||||
pin: 11
|
||||
gpiochip: 0
|
||||
line: 11
|
||||
# Ant_sw: # GPIO1_C2 (IO3, physical 16)
|
||||
# pin: 50
|
||||
# gpiochip: 1
|
||||
# line: 18
|
||||
Enable_Pins:
|
||||
- pin: 2 # GPIO0_A2 (physical 37)
|
||||
gpiochip: 0
|
||||
line: 2
|
||||
- pin: 50 # GPIO1_C2 (physical 16)
|
||||
gpiochip: 1
|
||||
line: 18
|
||||
spidev: spidev0.1
|
||||
# CS: # GPIO0_A7 (SPI1_CSN1, physical 26)
|
||||
# pin: 7
|
||||
# gpiochip: 0
|
||||
# line: 7
|
||||
TX_GAIN_LORA: [9, 9, 10, 11, 9, 8, 9, 10, 10, 10, 11, 12, 12, 12, 12, 12, 12, 12, 12, 10, 9, 8]
|
||||
@@ -0,0 +1,30 @@
|
||||
Meta:
|
||||
name: Waveshare SX1262
|
||||
support: deprecated
|
||||
compatible:
|
||||
- ebyte-ecb41-pge # Armbian
|
||||
|
||||
Lora:
|
||||
Module: sx1262 # Waveshare SX126X XXXM
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
CS: # GPIO0_A1 (physical 40)
|
||||
pin: 1
|
||||
gpiochip: 0
|
||||
line: 1
|
||||
IRQ: # GPIO0_A3 (physical 36)
|
||||
pin: 3
|
||||
gpiochip: 0
|
||||
line: 3
|
||||
Busy: # GPIO0_A0 (physical 38)
|
||||
pin: 0
|
||||
gpiochip: 0
|
||||
line: 0
|
||||
Reset: # GPIO0_B4 (physical 12)
|
||||
pin: 12
|
||||
gpiochip: 0
|
||||
line: 12
|
||||
SX126X_ANT_SW: # GPIO1_B2 (physical 31)
|
||||
pin: 42
|
||||
gpiochip: 1
|
||||
line: 10
|
||||
spidev: spidev0.0
|
||||
@@ -0,0 +1,31 @@
|
||||
# For use with Armbian luckfox-pico-max
|
||||
# Waveshare LoRa HAT for Raspberry Pi Pico
|
||||
# https://www.waveshare.com/wiki/Pico-LoRa-SX1262
|
||||
|
||||
Meta:
|
||||
name: luckfox-pico-max-ws-raspberry-pi-pico-hat
|
||||
support: community
|
||||
compatible:
|
||||
- luckfox-pico-max # Armbian
|
||||
|
||||
Lora:
|
||||
Module: sx1262
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
spidev: spidev0.0
|
||||
Busy: # GPIO1_C7 / GP2
|
||||
pin: 55
|
||||
gpiochip: 1
|
||||
line: 23
|
||||
CS: # GPIO1_C6 / GP3
|
||||
pin: 54
|
||||
gpiochip: 1
|
||||
line: 22
|
||||
Reset: # GPIO1_D1 / GP15
|
||||
pin: 57
|
||||
gpiochip: 1
|
||||
line: 25
|
||||
IRQ: # GPIO2_A2 / GP20
|
||||
pin: 66
|
||||
gpiochip: 2
|
||||
line: 2
|
||||
@@ -0,0 +1,41 @@
|
||||
# !! WARNING: Hats on the OK3506 board are installed "backwards" (facing outwards)
|
||||
|
||||
# MeshAdv-Pi E22-900M30S
|
||||
# https://github.com/chrismyers2000/MeshAdv-Pi-Hat
|
||||
Meta:
|
||||
name: MeshAdv-Pi E22-900M30S
|
||||
support: community
|
||||
compatible:
|
||||
- forlinx-ok3506-s12 # Armbian
|
||||
|
||||
Lora:
|
||||
Module: sx1262
|
||||
CS: # GPIO0_B0 (physical 40)
|
||||
pin: 8
|
||||
gpiochip: 0
|
||||
line: 8
|
||||
IRQ: # GPIO3_B0 (physical 36)
|
||||
pin: 104
|
||||
gpiochip: 3
|
||||
line: 8
|
||||
Busy: # GPIO0_B1 (physical 38)
|
||||
pin: 9
|
||||
gpiochip: 0
|
||||
line: 9
|
||||
Reset: # GPIO0_B6 (physical 12)
|
||||
pin: 14
|
||||
gpiochip: 0
|
||||
line: 14
|
||||
TXen: # GPIO0_A3 (physical 33)
|
||||
pin: 3
|
||||
gpiochip: 0
|
||||
line: 3
|
||||
RXen: # GPIO0_A2 (physical 32)
|
||||
pin: 2
|
||||
gpiochip: 0
|
||||
line: 2
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
# Only for E22-900M33S:
|
||||
# Limit the output power to 8 dBm
|
||||
# SX126X_MAX_POWER: 8
|
||||
spidev: spidev0.0
|
||||
@@ -0,0 +1,35 @@
|
||||
# !! WARNING: Hats on the OK3506 board are installed "backwards" (facing outwards)
|
||||
|
||||
# MeshAdv Mini E22-900M22S
|
||||
# https://github.com/chrismyers2000/MeshAdv-Mini
|
||||
Meta:
|
||||
name: MeshAdv Mini E22-900M22S
|
||||
support: community
|
||||
compatible:
|
||||
- forlinx-ok3506-s12 # Armbian
|
||||
|
||||
Lora:
|
||||
Module: sx1262 # Ebyte E22-900M22S
|
||||
CS: # GPIO0_C3 (physical 24, SPI0_CSN0)
|
||||
pin: 19
|
||||
gpiochip: 0
|
||||
line: 19
|
||||
IRQ: # GPIO3_B0 (physical 36)
|
||||
pin: 104
|
||||
gpiochip: 3
|
||||
line: 8
|
||||
Busy: # GPIO0_B1 (physical 38)
|
||||
pin: 9
|
||||
gpiochip: 0
|
||||
line: 9
|
||||
Reset: # GPIO3_A6 (physical 18)
|
||||
pin: 102
|
||||
gpiochip: 3
|
||||
line: 6
|
||||
RXen: # GPIO0_A2 (physical 32)
|
||||
pin: 2
|
||||
gpiochip: 0
|
||||
line: 2
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
spidev: spidev0.0
|
||||
@@ -0,0 +1,40 @@
|
||||
# !! WARNING: Hats on the OK3506 board are installed "backwards" (facing outwards)
|
||||
|
||||
Meta:
|
||||
name: RAK6421 + RAK13300 Slot 1
|
||||
support: community # Promote when tested
|
||||
compatible:
|
||||
- forlinx-ok3506-s12 # Armbian
|
||||
|
||||
Lora:
|
||||
Module: sx1262
|
||||
IRQ: # GPIO3_B5 (IO6, physical 15)
|
||||
pin: 109
|
||||
gpiochip: 3
|
||||
line: 13
|
||||
Reset: # GPIO3_B0 (IO4, physical 36)
|
||||
pin: 104
|
||||
gpiochip: 3
|
||||
line: 8
|
||||
Busy: # GPIO3_A6 (IO5, physical 18)
|
||||
pin: 102
|
||||
gpiochip: 3
|
||||
line: 6
|
||||
# Ant_sw: # GPIO0_A3 (IO3, physical 33)
|
||||
# pin: 3
|
||||
# gpiochip: 0
|
||||
# line: 3
|
||||
Enable_Pins:
|
||||
- pin: 2 # GPIO0_A2 (physical 32)
|
||||
gpiochip: 0
|
||||
line: 2
|
||||
- pin: 3 # GPIO0_A3 (physical 33)
|
||||
gpiochip: 0
|
||||
line: 3
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: spidev0.0
|
||||
# CS: # GPIO0_C3 (SPI0_CSN0, physical 24)
|
||||
# pin: 19
|
||||
# gpiochip: 0
|
||||
# line: 19
|
||||
@@ -0,0 +1,38 @@
|
||||
# !! WARNING: Hats on the OK3506 board are installed "backwards" (facing outwards)
|
||||
|
||||
Meta:
|
||||
name: RAK6421 + RAK13300 Slot 2
|
||||
support: community # Promote when tested
|
||||
compatible:
|
||||
- forlinx-ok3506-s12 # Armbian
|
||||
|
||||
Lora:
|
||||
Module: sx1262
|
||||
IRQ: # GPIO0_B6 (IO6, physical 12)
|
||||
pin: 14
|
||||
gpiochip: 0
|
||||
line: 14
|
||||
Reset: # GPIO3_A6 (IO4, physical 18)
|
||||
pin: 102
|
||||
gpiochip: 3
|
||||
line: 6
|
||||
Busy: # GPIO0_B2 (IO5, physical 35)
|
||||
pin: 10
|
||||
gpiochip: 0
|
||||
line: 10
|
||||
# Ant_sw: # GPIO3_A7 (IO3, physical 16)
|
||||
# pin: 103
|
||||
# gpiochip: 3
|
||||
# line: 7
|
||||
Enable_Pins:
|
||||
- pin: 106 # GPIO3_B2 (physical 37)
|
||||
gpiochip: 3
|
||||
line: 10
|
||||
- pin: 103 # GPIO3_A7 (physical 16)
|
||||
gpiochip: 3
|
||||
line: 7
|
||||
spidev: spidev0.1
|
||||
# CS: # GPIO0_B7 (SPI0_CSN1, physical 26)
|
||||
# pin: 15
|
||||
# gpiochip: 0
|
||||
# line: 15
|
||||
@@ -0,0 +1,41 @@
|
||||
# !! WARNING: Hats on the OK3506 board are installed "backwards" (facing outwards)
|
||||
|
||||
Meta:
|
||||
name: RAK6421 + RAK13302 Slot 1
|
||||
support: community # Promote when tested
|
||||
compatible:
|
||||
- forlinx-ok3506-s12 # Armbian
|
||||
|
||||
Lora:
|
||||
Module: sx1262
|
||||
IRQ: # GPIO3_B5 (IO6, physical 15)
|
||||
pin: 109
|
||||
gpiochip: 3
|
||||
line: 13
|
||||
Reset: # GPIO3_B0 (IO4, physical 36)
|
||||
pin: 104
|
||||
gpiochip: 3
|
||||
line: 8
|
||||
Busy: # GPIO3_A6 (IO5, physical 18)
|
||||
pin: 102
|
||||
gpiochip: 3
|
||||
line: 6
|
||||
# Ant_sw: # GPIO0_A3 (IO3, physical 33)
|
||||
# pin: 3
|
||||
# gpiochip: 0
|
||||
# line: 3
|
||||
Enable_Pins:
|
||||
- pin: 2 # GPIO0_A2 (physical 32)
|
||||
gpiochip: 0
|
||||
line: 2
|
||||
- pin: 3 # GPIO0_A3 (physical 33)
|
||||
gpiochip: 0
|
||||
line: 3
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: spidev0.0
|
||||
# CS: # GPIO0_C3 (SPI0_CSN0, physical 24)
|
||||
# pin: 19
|
||||
# gpiochip: 0
|
||||
# line: 19
|
||||
TX_GAIN_LORA: [9, 9, 10, 11, 9, 8, 9, 10, 10, 10, 11, 12, 12, 12, 12, 12, 12, 12, 12, 10, 9, 8]
|
||||
@@ -0,0 +1,39 @@
|
||||
# !! WARNING: Hats on the OK3506 board are installed "backwards" (facing outwards)
|
||||
|
||||
Meta:
|
||||
name: RAK6421 + RAK13302 Slot 2
|
||||
support: community # Promote when tested
|
||||
compatible:
|
||||
- forlinx-ok3506-s12 # Armbian
|
||||
|
||||
Lora:
|
||||
Module: sx1262
|
||||
IRQ: # GPIO0_B6 (IO6, physical 12)
|
||||
pin: 14
|
||||
gpiochip: 0
|
||||
line: 14
|
||||
Reset: # GPIO3_A6 (IO4, physical 18)
|
||||
pin: 102
|
||||
gpiochip: 3
|
||||
line: 6
|
||||
Busy: # GPIO0_B2 (IO5, physical 35)
|
||||
pin: 10
|
||||
gpiochip: 0
|
||||
line: 10
|
||||
# Ant_sw: # GPIO3_A7 (IO3, physical 16)
|
||||
# pin: 103
|
||||
# gpiochip: 3
|
||||
# line: 7
|
||||
Enable_Pins:
|
||||
- pin: 106 # GPIO3_B2 (physical 37)
|
||||
gpiochip: 3
|
||||
line: 10
|
||||
- pin: 103 # GPIO3_A7 (physical 16)
|
||||
gpiochip: 3
|
||||
line: 7
|
||||
spidev: spidev0.1
|
||||
# CS: # GPIO0_B7 (SPI0_CSN1, physical 26)
|
||||
# pin: 15
|
||||
# gpiochip: 0
|
||||
# line: 15
|
||||
TX_GAIN_LORA: [9, 9, 10, 11, 9, 8, 9, 10, 10, 10, 11, 12, 12, 12, 12, 12, 12, 12, 12, 10, 9, 8]
|
||||
@@ -0,0 +1,32 @@
|
||||
# !! WARNING: Hats on the OK3506 board are installed "backwards" (facing outwards)
|
||||
|
||||
Meta:
|
||||
name: Waveshare SX1262
|
||||
support: deprecated
|
||||
compatible:
|
||||
- forlinx-ok3506-s12 # Armbian
|
||||
|
||||
Lora:
|
||||
Module: sx1262 # Waveshare SX126X XXXM
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
CS: # GPIO0_B0 (physical 40)
|
||||
pin: 8
|
||||
gpiochip: 0
|
||||
line: 8
|
||||
IRQ: # GPIO3_B0 (physical 36)
|
||||
pin: 104
|
||||
gpiochip: 3
|
||||
line: 8
|
||||
Busy: # GPIO0_B1 (physical 38)
|
||||
pin: 9
|
||||
gpiochip: 0
|
||||
line: 9
|
||||
Reset: # GPIO0_B6 (physical 12)
|
||||
pin: 14
|
||||
gpiochip: 0
|
||||
line: 14
|
||||
SX126X_ANT_SW: # GPIO3_B3 (physical 31)
|
||||
pin: 107
|
||||
gpiochip: 3
|
||||
line: 11
|
||||
spidev: spidev0.0
|
||||
@@ -1,25 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate release notes from merged PRs on develop and master branches.
|
||||
Categorizes PRs into Enhancements and Bug Fixes/Maintenance sections.
|
||||
"""
|
||||
"""Generate release notes from the actual release commit range."""
|
||||
|
||||
import subprocess
|
||||
import re
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def get_last_release_tag():
|
||||
"""Get the most recent release tag."""
|
||||
def get_last_release_tag(compare_ref, exclude_tag=None):
|
||||
"""Get the most recent version tag merged into compare_ref."""
|
||||
result = subprocess.run(
|
||||
["git", "describe", "--tags", "--abbrev=0"],
|
||||
["git", "tag", "--merged", compare_ref, "--sort=-version:refname", "v*"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
|
||||
for line in result.stdout.splitlines():
|
||||
candidate = line.strip()
|
||||
if not candidate:
|
||||
continue
|
||||
if exclude_tag and candidate == exclude_tag:
|
||||
continue
|
||||
return candidate
|
||||
|
||||
raise subprocess.CalledProcessError(result.returncode, result.args, output=result.stdout, stderr=result.stderr)
|
||||
|
||||
|
||||
def get_tag_date(tag):
|
||||
@@ -33,18 +39,18 @@ def get_tag_date(tag):
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def get_merged_prs_since_tag(tag, branch):
|
||||
"""Get all merged PRs since the given tag on the specified branch."""
|
||||
# Get commits since tag on the branch - look for PR numbers in parentheses
|
||||
def get_merged_prs_in_range(tag, compare_ref):
|
||||
"""Get all merged PRs in the git range between tag and compare_ref."""
|
||||
result = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"log",
|
||||
f"{tag}..origin/{branch}",
|
||||
f"{tag}..{compare_ref}",
|
||||
"--oneline",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
prs = []
|
||||
@@ -65,6 +71,25 @@ def get_merged_prs_since_tag(tag, branch):
|
||||
return prs
|
||||
|
||||
|
||||
def parse_args():
|
||||
"""Parse CLI arguments."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate release notes from the actual release commit range."
|
||||
)
|
||||
parser.add_argument("new_version", help="Version that will be tagged for this release")
|
||||
parser.add_argument(
|
||||
"--base-tag",
|
||||
dest="base_tag",
|
||||
help="Existing version tag to diff from. Defaults to the latest version tag merged into the compare ref.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--compare-ref",
|
||||
default="HEAD",
|
||||
help="Git ref to diff to. Defaults to HEAD.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def get_pr_details(pr_number):
|
||||
"""Get PR details from GitHub API via gh CLI."""
|
||||
try:
|
||||
@@ -268,28 +293,28 @@ def get_new_contributors(pr_details_list, tag, repo="meshtastic/firmware"):
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: generate_release_notes.py <new_version>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
new_version = sys.argv[1]
|
||||
args = parse_args()
|
||||
new_version = args.new_version
|
||||
compare_ref = args.compare_ref
|
||||
current_tag = f"v{new_version}"
|
||||
|
||||
# Get last release tag
|
||||
try:
|
||||
last_tag = get_last_release_tag()
|
||||
last_tag = args.base_tag or get_last_release_tag(compare_ref, exclude_tag=current_tag)
|
||||
except subprocess.CalledProcessError:
|
||||
print("Error: Could not find last release tag", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Collect PRs from both branches
|
||||
all_pr_numbers = set()
|
||||
print(
|
||||
f"Resolved release note range: {last_tag}..{compare_ref}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
for branch in ["develop", "master"]:
|
||||
try:
|
||||
prs = get_merged_prs_since_tag(last_tag, branch)
|
||||
all_pr_numbers.update(prs)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not get PRs from {branch}: {e}", file=sys.stderr)
|
||||
try:
|
||||
all_pr_numbers = set(get_merged_prs_in_range(last_tag, compare_ref))
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error: Could not get PRs for range {last_tag}..{compare_ref}: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Get details for all PRs
|
||||
enhancements = []
|
||||
|
||||
@@ -87,6 +87,9 @@
|
||||
</screenshots>
|
||||
|
||||
<releases>
|
||||
<release version="2.7.23" date="2026-04-14">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.23</url>
|
||||
</release>
|
||||
<release version="2.7.22" date="2026-04-06">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.22</url>
|
||||
</release>
|
||||
|
||||
Vendored
+6
@@ -1,3 +1,9 @@
|
||||
meshtasticd (2.7.23.0) unstable; urgency=medium
|
||||
|
||||
* Version 2.7.23
|
||||
|
||||
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Tue, 14 Apr 2026 12:29:48 +0000
|
||||
|
||||
meshtasticd (2.7.22.0) unstable; urgency=medium
|
||||
|
||||
* Version 2.7.22
|
||||
|
||||
Vendored
+1
@@ -14,6 +14,7 @@ Build-Depends: debhelper-compat (= 13),
|
||||
g++,
|
||||
pkg-config,
|
||||
libyaml-cpp-dev,
|
||||
libjsoncpp-dev,
|
||||
libgpiod-dev,
|
||||
libbluetooth-dev,
|
||||
libusb-1.0-0-dev,
|
||||
|
||||
@@ -34,6 +34,7 @@ BuildRequires: python3dist(grpcio-tools)
|
||||
BuildRequires: git-core
|
||||
BuildRequires: gcc-c++
|
||||
BuildRequires: pkgconfig(yaml-cpp)
|
||||
BuildRequires: pkgconfig(jsoncpp)
|
||||
BuildRequires: pkgconfig(libgpiod)
|
||||
BuildRequires: pkgconfig(bluez)
|
||||
BuildRequires: pkgconfig(libusb-1.0)
|
||||
|
||||
+93
-82
@@ -50,16 +50,13 @@ build_flags = -Wno-missing-field-initializers
|
||||
-DRADIOLIB_EXCLUDE_APRS=1
|
||||
-DRADIOLIB_EXCLUDE_LORAWAN=1
|
||||
-DMESHTASTIC_EXCLUDE_DROPZONE=1
|
||||
-DMESHTASTIC_EXCLUDE_ZPS=1
|
||||
-DMESHTASTIC_EXCLUDE_REPLYBOT=1
|
||||
-DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=1
|
||||
-DMESHTASTIC_EXCLUDE_HEALTH_TELEMETRY=1
|
||||
-DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware
|
||||
-DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1
|
||||
-DMESHTASTIC_EXCLUDE_POWERMON=1
|
||||
-DMESHTASTIC_EXCLUDE_STATUS=1
|
||||
-D MAX_THREADS=40 ; As we've split modules, we have more threads to manage
|
||||
-DLED_BUILTIN=-1
|
||||
#-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now
|
||||
#-D OLED_PL=1
|
||||
#-D DEBUG_HEAP=1 ; uncomment to add free heap space / memory leak debugging logs
|
||||
@@ -127,108 +124,122 @@ lib_deps =
|
||||
[device-ui_base]
|
||||
lib_deps =
|
||||
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
|
||||
https://github.com/meshtastic/device-ui/archive/1897dd17fceb1f101bb1a3245680aa3439edcfdd.zip
|
||||
https://github.com/meshtastic/device-ui/archive/5305670b68eb5b92d14e62b5b536969ca4bb441f.zip
|
||||
|
||||
; Common libs for environmental measurements in telemetry module
|
||||
[environmental_base]
|
||||
lib_deps =
|
||||
# renovate: datasource=custom.pio depName=Adafruit BusIO packageName=adafruit/library/Adafruit BusIO
|
||||
adafruit/Adafruit BusIO@1.17.4
|
||||
# renovate: datasource=custom.pio depName=Adafruit Unified Sensor packageName=adafruit/library/Adafruit Unified Sensor
|
||||
adafruit/Adafruit Unified Sensor@1.1.15
|
||||
# renovate: datasource=custom.pio depName=Adafruit BMP280 packageName=adafruit/library/Adafruit BMP280 Library
|
||||
adafruit/Adafruit BMP280 Library@3.0.0
|
||||
# renovate: datasource=custom.pio depName=Adafruit BMP085 packageName=adafruit/library/Adafruit BMP085 Library
|
||||
adafruit/Adafruit BMP085 Library@1.2.4
|
||||
# renovate: datasource=custom.pio depName=Adafruit BME280 packageName=adafruit/library/Adafruit BME280 Library
|
||||
adafruit/Adafruit BME280 Library@2.3.0
|
||||
# renovate: datasource=custom.pio depName=Adafruit DPS310 packageName=adafruit/library/Adafruit DPS310
|
||||
adafruit/Adafruit DPS310@1.1.6
|
||||
# renovate: datasource=custom.pio depName=Adafruit MCP9808 packageName=adafruit/library/Adafruit MCP9808 Library
|
||||
adafruit/Adafruit MCP9808 Library@2.0.2
|
||||
# renovate: datasource=custom.pio depName=Adafruit INA260 packageName=adafruit/library/Adafruit INA260 Library
|
||||
adafruit/Adafruit INA260 Library@1.5.3
|
||||
# renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219
|
||||
adafruit/Adafruit INA219@1.2.3
|
||||
# renovate: datasource=custom.pio depName=Adafruit MPU6050 packageName=adafruit/library/Adafruit MPU6050
|
||||
adafruit/Adafruit MPU6050@2.2.9
|
||||
# renovate: datasource=custom.pio depName=Adafruit LIS3DH packageName=adafruit/library/Adafruit LIS3DH
|
||||
adafruit/Adafruit LIS3DH@1.3.0
|
||||
# renovate: datasource=custom.pio depName=Adafruit AHTX0 packageName=adafruit/library/Adafruit AHTX0
|
||||
adafruit/Adafruit AHTX0@2.0.6
|
||||
# renovate: datasource=custom.pio depName=Adafruit LSM6DS packageName=adafruit/library/Adafruit LSM6DS
|
||||
adafruit/Adafruit LSM6DS@4.7.4
|
||||
# renovate: datasource=custom.pio depName=Adafruit TSL2591 packageName=adafruit/library/Adafruit TSL2591 Library
|
||||
adafruit/Adafruit TSL2591 Library@1.4.5
|
||||
# renovate: datasource=custom.pio depName=EmotiBit MLX90632 packageName=emotibit/library/EmotiBit MLX90632
|
||||
emotibit/EmotiBit MLX90632@1.0.8
|
||||
# renovate: datasource=custom.pio depName=Adafruit MLX90614 packageName=adafruit/library/Adafruit MLX90614 Library
|
||||
adafruit/Adafruit MLX90614 Library@2.1.6
|
||||
# renovate: datasource=github-tags depName=Adafruit BusIO packageName=adafruit/Adafruit_BusIO
|
||||
https://github.com/adafruit/Adafruit_BusIO/archive/refs/tags/1.17.4.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit Unified Sensor packageName=adafruit/Adafruit_Sensor
|
||||
https://github.com/adafruit/Adafruit_Sensor/archive/refs/tags/1.1.15.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit GFX packageName=adafruit/Adafruit-GFX-Library
|
||||
https://github.com/adafruit/Adafruit-GFX-Library/archive/refs/tags/1.12.6.zip
|
||||
# renovate: datasource=github-tags depName=NeoPixel packageName=adafruit/Adafruit_NeoPixel
|
||||
https://github.com/adafruit/Adafruit_NeoPixel/archive/refs/tags/1.15.4.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit SSD1306 packageName=adafruit/Adafruit_SSD1306
|
||||
https://github.com/adafruit/Adafruit_SSD1306/archive/refs/tags/2.5.16.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit BMP280 packageName=adafruit/Adafruit_BMP280_Library
|
||||
https://github.com/adafruit/Adafruit_BMP280_Library/archive/refs/tags/3.0.0.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit BMP085 packageName=adafruit/Adafruit-BMP085-Library
|
||||
https://github.com/adafruit/Adafruit-BMP085-Library/archive/refs/tags/1.2.4.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit BME280 packageName=adafruit/Adafruit_BME280_Library
|
||||
https://github.com/adafruit/Adafruit_BME280_Library/archive/refs/tags/2.3.0.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit DPS310 packageName=adafruit/Adafruit_DPS310
|
||||
https://github.com/adafruit/Adafruit_DPS310/archive/refs/tags/1.1.6.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit SH110x packageName=adafruit/Adafruit_SH110x
|
||||
https://github.com/adafruit/Adafruit_SH110x/archive/refs/tags/2.1.14.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit MCP9808 packageName=adafruit/Adafruit_MCP9808_Library
|
||||
https://github.com/adafruit/Adafruit_MCP9808_Library/archive/refs/tags/2.0.2.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit INA260 packageName=adafruit/Adafruit_INA260
|
||||
https://github.com/adafruit/Adafruit_INA260/archive/refs/tags/1.5.3.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit INA219 packageName=adafruit/Adafruit_INA219
|
||||
https://github.com/adafruit/Adafruit_INA219/archive/refs/tags/1.2.3.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit MPU6050 packageName=adafruit/Adafruit_MPU6050
|
||||
https://github.com/adafruit/Adafruit_MPU6050/archive/refs/tags/2.2.9.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit LIS3DH packageName=adafruit/Adafruit_LIS3DH
|
||||
https://github.com/adafruit/Adafruit_LIS3DH/archive/refs/tags/1.3.0.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit AHTX0 packageName=adafruit/Adafruit_AHTX0
|
||||
https://github.com/adafruit/Adafruit_AHTX0/archive/refs/tags/2.0.6.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit LSM6DS packageName=adafruit/Adafruit_LSM6DS
|
||||
https://github.com/adafruit/Adafruit_LSM6DS/archive/refs/tags/4.7.4.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit TSL2591 packageName=adafruit/Adafruit_TSL2591_Library
|
||||
https://github.com/adafruit/Adafruit_TSL2591_Library/archive/refs/tags/1.4.5.zip
|
||||
# renovate: datasource=github-tags depName=EmotiBit MLX90632 packageName=emotibit/EmotiBit_MLX90632
|
||||
https://github.com/EmotiBit/EmotiBit_MLX90632/archive/refs/tags/v1.0.8.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit MLX90614 packageName=adafruit/Adafruit_MLX90614
|
||||
https://github.com/adafruit/Adafruit-MLX90614-Library/archive/refs/tags/2.1.6.zip
|
||||
# renovate: datasource=git-refs depName=INA3221 packageName=https://github.com/sgtwilko/INA3221 gitBranch=FixOverflow
|
||||
https://github.com/sgtwilko/INA3221/archive/bb03d7e9bfcc74fc798838a54f4f99738f29fc6a.zip
|
||||
# renovate: datasource=custom.pio depName=QMC5883L Compass packageName=mprograms/library/QMC5883LCompass
|
||||
mprograms/QMC5883LCompass@1.2.3
|
||||
# renovate: datasource=custom.pio depName=DFRobot_RTU packageName=dfrobot/library/DFRobot_RTU
|
||||
dfrobot/DFRobot_RTU@1.0.6
|
||||
# renovate: datasource=github-tags depName=QMC5883L Compass packageName=mprograms/QMC5883LCompass
|
||||
https://github.com/mprograms/QMC5883LCompass/archive/refs/tags/v1.2.3.zip
|
||||
# renovate: datasource=github-tags depName=DFRobot_RTU packageName=dfrobot/DFRobot_RTU
|
||||
https://github.com/DFRobot/DFRobot_RTU/archive/refs/tags/V1.0.6.zip
|
||||
# renovate: datasource=git-refs depName=DFRobot_RainfallSensor packageName=https://github.com/DFRobot/DFRobot_RainfallSensor gitBranch=master
|
||||
https://github.com/DFRobot/DFRobot_RainfallSensor/archive/38fea5e02b40a5430be6dab39a99a6f6347d667e.zip
|
||||
# renovate: datasource=custom.pio depName=INA226 packageName=robtillaart/library/INA226
|
||||
robtillaart/INA226@0.6.6
|
||||
# renovate: datasource=custom.pio depName=SparkFun MAX3010x packageName=sparkfun/library/SparkFun MAX3010x Pulse and Proximity Sensor Library
|
||||
sparkfun/SparkFun MAX3010x Pulse and Proximity Sensor Library@1.1.2
|
||||
# renovate: datasource=custom.pio depName=SparkFun 9DoF IMU Breakout ICM 20948 packageName=sparkfun/library/SparkFun 9DoF IMU Breakout - ICM 20948 - Arduino Library
|
||||
sparkfun/SparkFun 9DoF IMU Breakout - ICM 20948 - Arduino Library@1.3.2
|
||||
# renovate: datasource=custom.pio depName=Adafruit LTR390 Library packageName=adafruit/library/Adafruit LTR390 Library
|
||||
adafruit/Adafruit LTR390 Library@1.1.2
|
||||
# renovate: datasource=custom.pio depName=Adafruit PCT2075 packageName=adafruit/library/Adafruit PCT2075
|
||||
adafruit/Adafruit PCT2075@1.0.6
|
||||
# renovate: datasource=custom.pio depName=DFRobot_BMM150 packageName=dfrobot/library/DFRobot_BMM150
|
||||
dfrobot/DFRobot_BMM150@1.0.0
|
||||
# renovate: datasource=custom.pio depName=Adafruit_TSL2561 packageName=adafruit/library/Adafruit TSL2561
|
||||
adafruit/Adafruit TSL2561@1.1.3
|
||||
# renovate: datasource=custom.pio depName=BH1750_WE packageName=wollewald/library/BH1750_WE
|
||||
wollewald/BH1750_WE@1.1.10
|
||||
# renovate: datasource=github-tags depName=INA226 packageName=robtillaart/INA226
|
||||
https://github.com/RobTillaart/INA226/archive/refs/tags/0.6.6.zip
|
||||
# renovate: datasource=github-tags depName=SparkFun MAX3010x packageName=sparkfun/SparkFun_MAX3010x_Sensor_Library
|
||||
https://github.com/sparkfun/SparkFun_MAX3010x_Sensor_Library/archive/refs/tags/v1.1.2.zip
|
||||
# renovate: datasource=github-tags depName=SparkFun 9DoF IMU Breakout ICM 20948 packageName=sparkfun/SparkFun_ICM-20948_ArduinoLibrary
|
||||
https://github.com/sparkfun/SparkFun_ICM-20948_ArduinoLibrary/archive/refs/tags/v1.3.2.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit LTR390 Library packageName=adafruit/Adafruit_LTR390
|
||||
https://github.com/adafruit/Adafruit_LTR390/archive/refs/tags/1.1.2.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit PCT2075 packageName=adafruit/Adafruit_PCT2075
|
||||
https://github.com/adafruit/Adafruit_PCT2075/archive/refs/tags/1.0.6.zip
|
||||
# renovate: datasource=github-tags depName=DFRobot_BMM150 packageName=dfrobot/DFRobot_BMM150
|
||||
https://github.com/DFRobot/DFRobot_BMM150/archive/refs/tags/V1.0.0.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit_TSL2561 packageName=adafruit/Adafruit_TSL2561
|
||||
https://github.com/adafruit/Adafruit_TSL2561/archive/refs/tags/1.1.3.zip
|
||||
# renovate: datasource=github-tags depName=BH1750_WE packageName=wollewald/BH1750_WE
|
||||
https://github.com/wollewald/BH1750_WE/archive/refs/tags/1.1.10.zip
|
||||
|
||||
; Common environmental sensor libraries (not included in native / portduino)
|
||||
[environmental_extra_common]
|
||||
lib_deps =
|
||||
# renovate: datasource=custom.pio depName=Adafruit BMP3XX packageName=adafruit/library/Adafruit BMP3XX Library
|
||||
adafruit/Adafruit BMP3XX Library@2.1.6
|
||||
# renovate: datasource=custom.pio depName=Adafruit MAX1704X packageName=adafruit/library/Adafruit MAX1704X
|
||||
adafruit/Adafruit MAX1704X@1.0.3
|
||||
# renovate: datasource=custom.pio depName=Adafruit LPS2X packageName=adafruit/library/Adafruit LPS2X
|
||||
adafruit/Adafruit LPS2X@2.0.6
|
||||
# renovate: datasource=custom.pio depName=Adafruit VEML7700 packageName=adafruit/library/Adafruit VEML7700 Library
|
||||
adafruit/Adafruit VEML7700 Library@2.1.6
|
||||
# renovate: datasource=custom.pio depName=SparkFun Qwiic Scale NAU7802 packageName=sparkfun/library/SparkFun Qwiic Scale NAU7802 Arduino Library
|
||||
sparkfun/SparkFun Qwiic Scale NAU7802 Arduino Library@1.0.6
|
||||
# renovate: datasource=github-tags depName=Adafruit BMP3XX packageName=adafruit/Adafruit_BMP3XX
|
||||
https://github.com/adafruit/Adafruit_BMP3XX/archive/refs/tags/2.1.6.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit MAX1704X packageName=adafruit/Adafruit_MAX1704X
|
||||
https://github.com/adafruit/Adafruit_MAX1704X/archive/refs/tags/1.0.3.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit SHTC3 packageName=adafruit/Adafruit_SHTC3
|
||||
https://github.com/adafruit/Adafruit_SHTC3/archive/refs/tags/1.0.2.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit LPS2X packageName=adafruit/Adafruit_LPS2X
|
||||
https://github.com/adafruit/Adafruit_LPS2X/archive/refs/tags/2.0.6.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit SHT31 packageName=adafruit/Adafruit_SHT31
|
||||
https://github.com/adafruit/Adafruit_SHT31/archive/refs/tags/2.2.2.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit VEML7700 packageName=adafruit/Adafruit_VEML7700
|
||||
https://github.com/adafruit/Adafruit_VEML7700/archive/refs/tags/2.1.6.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit SHT4x packageName=adafruit/Adafruit_SHT4X
|
||||
https://github.com/adafruit/Adafruit_SHT4X/archive/refs/tags/1.0.5.zip
|
||||
# renovate: datasource=github-tags depName=SparkFun Qwiic Scale NAU7802 packageName=sparkfun/SparkFun_Qwiic_Scale_NAU7802_Arduino_Library
|
||||
https://github.com/sparkfun/SparkFun_Qwiic_Scale_NAU7802_Arduino_Library/archive/refs/tags/v1.0.6.zip
|
||||
# renovate: datasource=custom.pio depName=ClosedCube OPT3001 packageName=closedcube/library/ClosedCube OPT3001
|
||||
closedcube/ClosedCube OPT3001@1.1.2
|
||||
# renovate: datasource=git-refs depName=meshtastic-DFRobot_LarkWeatherStation packageName=https://github.com/meshtastic/DFRobot_LarkWeatherStation gitBranch=master
|
||||
https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip
|
||||
# renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core
|
||||
sensirion/Sensirion Core@0.7.3
|
||||
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
|
||||
sensirion/Sensirion I2C SCD4x@1.1.0
|
||||
# renovate: datasource=custom.pio depName=Sensirion I2C SFA3x packageName=sensirion/library/Sensirion I2C SFA3x
|
||||
sensirion/Sensirion I2C SFA3x@1.0.0
|
||||
# renovate: datasource=custom.pio depName=Sensirion I2C SCD30 packageName=sensirion/library/Sensirion I2C SCD30
|
||||
sensirion/Sensirion I2C SCD30@1.0.0
|
||||
# renovate: datasource=custom.pio depName=arduino-sht packageName=sensirion/library/arduino-sht
|
||||
sensirion/arduino-sht@1.2.6
|
||||
# renovate: datasource=github-tags depName=Sensirion Core packageName=sensirion/arduino-core
|
||||
https://github.com/Sensirion/arduino-core/archive/refs/tags/0.7.3.zip
|
||||
# renovate: datasource=github-tags depName=Sensirion I2C SCD4x packageName=sensirion/arduino-i2c-scd4x
|
||||
https://github.com/Sensirion/arduino-i2c-scd4x/archive/refs/tags/1.1.0.zip
|
||||
# renovate: datasource=github-tags depName=Sensirion I2C SFA3x packageName=sensirion/arduino-i2c-sfa3x
|
||||
https://github.com/Sensirion/arduino-i2c-sfa3x/archive/refs/tags/1.0.0.zip
|
||||
# renovate: datasource=github-tags depName=Sensirion I2C SCD30 packageName=sensirion/arduino-i2c-scd30
|
||||
https://github.com/Sensirion/arduino-i2c-scd30/archive/refs/tags/1.0.0.zip
|
||||
# renovate: datasource=github-tags depName=arduino-sht packageName=sensirion/arduino-sht
|
||||
https://github.com/Sensirion/arduino-sht/archive/refs/tags/v1.2.6.zip
|
||||
|
||||
; Environmental sensors with BSEC2 (Bosch proprietary IAQ)
|
||||
[environmental_extra]
|
||||
lib_deps =
|
||||
${environmental_extra_common.lib_deps}
|
||||
# renovate: datasource=custom.pio depName=Bosch BSEC2 packageName=boschsensortec/library/bsec2
|
||||
boschsensortec/bsec2@1.10.2610
|
||||
# renovate: datasource=custom.pio depName=Bosch BME68x packageName=boschsensortec/library/BME68x Sensor Library
|
||||
boschsensortec/BME68x Sensor Library@1.3.40408
|
||||
# renovate: datasource=github-tags depName=Bosch BSEC2 packageName=boschsensortec/Bosch-BSEC2-Library
|
||||
https://github.com/boschsensortec/Bosch-BSEC2-Library/archive/refs/tags/1.10.2610.zip
|
||||
# renovate: datasource=github-tags depName=Bosch BME68x packageName=boschsensortec/Bosch-BME68x-Library
|
||||
https://github.com/boschsensortec/Bosch-BME68x-Library/archive/refs/tags/v1.3.40408.zip
|
||||
|
||||
; Environmental sensors without BSEC (saves ~3.5KB DRAM for original ESP32 targets)
|
||||
[environmental_extra_no_bsec]
|
||||
lib_deps =
|
||||
${environmental_extra_common.lib_deps}
|
||||
# renovate: datasource=custom.pio depName=Adafruit_BME680 packageName=adafruit/library/Adafruit BME680 Library
|
||||
adafruit/Adafruit BME680 Library@2.0.6
|
||||
# renovate: datasource=github-tags depName=Adafruit_BME680 packageName=adafruit/Adafruit_BME680
|
||||
https://github.com/adafruit/Adafruit_BME680/archive/refs/tags/2.0.6.zip
|
||||
|
||||
@@ -21,8 +21,15 @@
|
||||
// How many messages are stored (RAM + flash).
|
||||
// Define -DMESSAGE_HISTORY_LIMIT=N in build_flags to control memory usage.
|
||||
#ifndef MESSAGE_HISTORY_LIMIT
|
||||
#if defined(ARCH_ESP32) && \
|
||||
!(defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32S2))
|
||||
// Baseline ESP32 (non-PSRAM variants) has limited heap; reduce message history on resource-constrained builds.
|
||||
// Override with -DMESSAGE_HISTORY_LIMIT=N if needed.
|
||||
#define MESSAGE_HISTORY_LIMIT 10
|
||||
#else
|
||||
#define MESSAGE_HISTORY_LIMIT 20
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// Internal alias used everywhere in code – do NOT redefine elsewhere.
|
||||
#define MAX_MESSAGES_SAVED MESSAGE_HISTORY_LIMIT
|
||||
|
||||
+34
-5
@@ -40,6 +40,22 @@
|
||||
#include "concurrency/LockGuard.h"
|
||||
#endif
|
||||
|
||||
#if defined(ARCH_STM32WL) && defined(BATTERY_PIN)
|
||||
#include "stm32yyxx_ll_adc.h"
|
||||
|
||||
/* Analog read resolution */
|
||||
#if defined(LL_ADC_RESOLUTION_12B)
|
||||
#define LL_ADC_RESOLUTION LL_ADC_RESOLUTION_12B
|
||||
#define BATTERY_SENSE_RESOLUTION_BITS 12
|
||||
#elif defined(LL_ADC_DS_DATA_WIDTH_12_BIT)
|
||||
#define LL_ADC_RESOLUTION LL_ADC_DS_DATA_WIDTH_12_BIT
|
||||
#define BATTERY_SENSE_RESOLUTION_BITS 12
|
||||
#else
|
||||
#error "ADC resolution could not be defined!"
|
||||
#endif
|
||||
#define ADC_RANGE (1 << BATTERY_SENSE_RESOLUTION_BITS)
|
||||
#endif
|
||||
|
||||
#if defined(DEBUG_HEAP_MQTT) && !MESHTASTIC_EXCLUDE_MQTT
|
||||
#include "mqtt/MQTT.h"
|
||||
#include "target_specific.h"
|
||||
@@ -328,11 +344,17 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
float scaled = 0;
|
||||
|
||||
battery_adcEnable();
|
||||
#ifdef ARCH_ESP32 // ADC block for espressif platforms
|
||||
#ifdef ARCH_STM32WL
|
||||
// STM32 ADC with VREFINT runtime calibration
|
||||
Vref = __LL_ADC_CALC_VREFANALOG_VOLTAGE(analogRead(AVREF), LL_ADC_RESOLUTION);
|
||||
raw = analogRead(BATTERY_PIN);
|
||||
scaled = __LL_ADC_CALC_DATA_TO_VOLTAGE(Vref, raw, LL_ADC_RESOLUTION);
|
||||
scaled *= operativeAdcMultiplier;
|
||||
#elif defined(ARCH_ESP32) // ADC block for espressif platforms
|
||||
raw = espAdcRead();
|
||||
scaled = esp_adc_cal_raw_to_voltage(raw, adc_characs);
|
||||
scaled *= operativeAdcMultiplier;
|
||||
#else // block for all other platforms
|
||||
#else // block for all other platforms
|
||||
#ifdef ARCH_NRF52
|
||||
concurrency::LockGuard saadcGuard(concurrency::nrf52SaadcLock);
|
||||
#endif
|
||||
@@ -530,6 +552,11 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
bool initial_read_done = false;
|
||||
float last_read_value = (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS);
|
||||
uint32_t last_read_time_ms = 0;
|
||||
#ifdef ARCH_STM32WL
|
||||
// 3300mV placeholder for STM32 errata where VREFINT factory calibration may be missing
|
||||
// (e.g. STM32U0, see DS14756 Rev 3 §2.4.1 "VREFINT offset")
|
||||
uint32_t Vref = 3300;
|
||||
#endif
|
||||
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT)
|
||||
|
||||
@@ -639,7 +666,9 @@ bool Power::analogInit()
|
||||
#define BATTERY_SENSE_RESOLUTION_BITS 10
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_ESP32 // ESP32 needs special analog stuff
|
||||
#ifdef ARCH_STM32WL
|
||||
analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS);
|
||||
#elif defined(ARCH_ESP32) // ESP32 needs special analog stuff
|
||||
|
||||
#ifndef ADC_WIDTH // max resolution by default
|
||||
static const adc_bits_width_t width = ADC_WIDTH_BIT_12;
|
||||
@@ -649,7 +678,7 @@ bool Power::analogInit()
|
||||
#ifndef BAT_MEASURE_ADC_UNIT // ADC1
|
||||
adc1_config_width(width);
|
||||
adc1_config_channel_atten(adc_channel, atten);
|
||||
#else // ADC2
|
||||
#else // ADC2
|
||||
adc2_config_channel_atten(adc_channel, atten);
|
||||
#ifndef CONFIG_IDF_TARGET_ESP32S3
|
||||
// ADC2 wifi bug workaround
|
||||
@@ -679,7 +708,7 @@ bool Power::analogInit()
|
||||
|
||||
// NRF52 ADC init moved to powerHAL_init in nrf52 platform
|
||||
|
||||
#ifndef ARCH_ESP32
|
||||
#if !defined(ARCH_ESP32) && !defined(ARCH_STM32WL)
|
||||
analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS);
|
||||
#endif
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ size_t RedirectablePrint::write(uint8_t c)
|
||||
size_t RedirectablePrint::vprintf(const char *logLevel, const char *format, va_list arg)
|
||||
{
|
||||
va_list copy;
|
||||
#if ENABLE_JSON_LOGGING || ARCH_PORTDUINO
|
||||
#if ARCH_PORTDUINO
|
||||
static char printBuf[512];
|
||||
#else
|
||||
static char printBuf[160];
|
||||
|
||||
@@ -30,6 +30,9 @@ SerialConsole *console;
|
||||
|
||||
void consoleInit()
|
||||
{
|
||||
if (console) {
|
||||
return;
|
||||
}
|
||||
auto sc = new SerialConsole(); // Must be dynamically allocated because we are now inheriting from thread
|
||||
|
||||
#if defined(SERIAL_HAS_ON_RECEIVE)
|
||||
|
||||
@@ -1,10 +1,85 @@
|
||||
#include "concurrency/BinarySemaphorePosix.h"
|
||||
#include "configuration.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <sys/time.h>
|
||||
|
||||
#ifndef HAS_FREE_RTOS
|
||||
|
||||
namespace concurrency
|
||||
{
|
||||
#ifdef ARCH_PORTDUINO
|
||||
|
||||
BinarySemaphorePosix::BinarySemaphorePosix()
|
||||
{
|
||||
if (pthread_mutex_init(&mutex, NULL) != 0) {
|
||||
throw std::runtime_error("pthread_mutex_init failed");
|
||||
}
|
||||
if (pthread_cond_init(&cond, NULL) != 0) {
|
||||
pthread_mutex_destroy(&mutex);
|
||||
throw std::runtime_error("pthread_cond_init failed");
|
||||
}
|
||||
signaled = false;
|
||||
}
|
||||
|
||||
BinarySemaphorePosix::~BinarySemaphorePosix()
|
||||
{
|
||||
pthread_cond_destroy(&cond);
|
||||
pthread_mutex_destroy(&mutex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns false if we timed out
|
||||
*/
|
||||
bool BinarySemaphorePosix::take(uint32_t msec)
|
||||
{
|
||||
pthread_mutex_lock(&mutex);
|
||||
|
||||
if (!signaled) {
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
|
||||
ts.tv_sec += msec / 1000;
|
||||
ts.tv_nsec += (msec % 1000) * 1000000L;
|
||||
if (ts.tv_nsec >= 1000000000L) {
|
||||
ts.tv_sec += 1;
|
||||
ts.tv_nsec -= 1000000000L;
|
||||
}
|
||||
|
||||
while (!signaled) {
|
||||
int rc = pthread_cond_timedwait(&cond, &mutex, &ts);
|
||||
if (rc == ETIMEDOUT)
|
||||
break;
|
||||
if (rc != 0) {
|
||||
// Some other error occurred
|
||||
pthread_mutex_unlock(&mutex);
|
||||
throw std::runtime_error("pthread_cond_timedwait failed: " + std::to_string(rc));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool wasSignaled = signaled;
|
||||
signaled = false; // consume the signal (binary semaphore)
|
||||
|
||||
pthread_mutex_unlock(&mutex);
|
||||
return wasSignaled;
|
||||
}
|
||||
|
||||
void BinarySemaphorePosix::give()
|
||||
{
|
||||
pthread_mutex_lock(&mutex);
|
||||
signaled = true;
|
||||
pthread_cond_signal(&cond);
|
||||
pthread_mutex_unlock(&mutex);
|
||||
}
|
||||
|
||||
IRAM_ATTR void BinarySemaphorePosix::giveFromISR(BaseType_t *pxHigherPriorityTaskWoken)
|
||||
{
|
||||
give();
|
||||
if (pxHigherPriorityTaskWoken)
|
||||
*pxHigherPriorityTaskWoken = true;
|
||||
}
|
||||
#else
|
||||
|
||||
BinarySemaphorePosix::BinarySemaphorePosix() {}
|
||||
|
||||
@@ -22,7 +97,8 @@ bool BinarySemaphorePosix::take(uint32_t msec)
|
||||
void BinarySemaphorePosix::give() {}
|
||||
|
||||
IRAM_ATTR void BinarySemaphorePosix::giveFromISR(BaseType_t *pxHigherPriorityTaskWoken) {}
|
||||
#endif
|
||||
|
||||
} // namespace concurrency
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
#include "../freertosinc.h"
|
||||
|
||||
#ifdef ARCH_PORTDUINO
|
||||
#include <pthread.h>
|
||||
#endif
|
||||
|
||||
namespace concurrency
|
||||
{
|
||||
|
||||
@@ -9,7 +13,12 @@ namespace concurrency
|
||||
|
||||
class BinarySemaphorePosix
|
||||
{
|
||||
// SemaphoreHandle_t semaphore;
|
||||
|
||||
#ifdef ARCH_PORTDUINO
|
||||
pthread_mutex_t mutex;
|
||||
pthread_cond_t cond;
|
||||
bool signaled;
|
||||
#endif
|
||||
|
||||
public:
|
||||
BinarySemaphorePosix();
|
||||
@@ -27,4 +36,4 @@ class BinarySemaphorePosix
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace concurrency
|
||||
} // namespace concurrency
|
||||
|
||||
@@ -202,7 +202,7 @@ void EInkParallelDisplay::display(void)
|
||||
|
||||
// Get pointers to internal buffers
|
||||
uint8_t *cur = epaper->currentBuffer();
|
||||
uint8_t *prev = epaper->previousBuffer(); // may be NULL on first init
|
||||
const uint8_t *prev = epaper->previousBuffer(); // may be NULL on first init
|
||||
|
||||
// Track changed row range while converting
|
||||
int newTop = h; // min changed row (initialized to out-of-range)
|
||||
|
||||
@@ -1197,7 +1197,7 @@ void Screen::setFrames(FrameFocus focus)
|
||||
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||
const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
|
||||
if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) {
|
||||
favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo);
|
||||
favoriteFrames.push_back(graphics::UIRenderer::drawFavoriteNode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1226,7 +1226,7 @@ void Screen::setFrames(FrameFocus focus)
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
|
||||
prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list just changed)
|
||||
prevFrame = -1; // Force drawFavoriteNode to pick a new node (because our list just changed)
|
||||
|
||||
// Focus on a specific frame, in the frame set we just created
|
||||
switch (focus) {
|
||||
|
||||
@@ -422,6 +422,17 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
std::vector<bool> isMine; // track alignment
|
||||
std::vector<bool> isHeader; // track header lines
|
||||
std::vector<AckStatus> ackForLine;
|
||||
// Hard limit on total cached lines to prevent unbounded growth from a single long message.
|
||||
// Reserve to the actual cache cap up front, because a single message can expand to many more
|
||||
// wrapped display lines than a small per-message estimate would predict. For a display
|
||||
// rendering only ~5-30 lines at a time, caching more than this limit wastes heap. Stop
|
||||
// appending once we reach MAX_CACHED_LINES to prevent a single message from blowing out the
|
||||
// heap.
|
||||
constexpr size_t MAX_CACHED_LINES = 100U; // ~5-6KB for std::string overhead on 32-bit (if each ~50-60 bytes avg)
|
||||
allLines.reserve(MAX_CACHED_LINES);
|
||||
isMine.reserve(MAX_CACHED_LINES);
|
||||
isHeader.reserve(MAX_CACHED_LINES);
|
||||
ackForLine.reserve(MAX_CACHED_LINES);
|
||||
|
||||
for (auto it = filtered.rbegin(); it != filtered.rend(); ++it) {
|
||||
const auto &m = *it;
|
||||
@@ -565,16 +576,23 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
|
||||
int wrapWidth = mine ? rightTextWidth : leftTextWidth;
|
||||
std::vector<std::string> wrapped = generateLines(display, "", msgText, wrapWidth);
|
||||
// Per-message wrap-line limit: even if wrapping produces many lines, cap them to prevent
|
||||
// a single long message from consuming most or all of the cache.
|
||||
constexpr size_t MAX_WRAPPED_LINES_PER_MSG = 20U;
|
||||
size_t wrappedCount = 0;
|
||||
for (auto &ln : wrapped) {
|
||||
allLines.push_back(ln);
|
||||
if (allLines.size() >= MAX_CACHED_LINES || wrappedCount >= MAX_WRAPPED_LINES_PER_MSG)
|
||||
break; // Cache limit or per-message limit reached; stop adding lines from this message
|
||||
allLines.emplace_back(std::move(ln));
|
||||
isMine.push_back(mine);
|
||||
isHeader.push_back(false);
|
||||
ackForLine.push_back(AckStatus::NONE);
|
||||
++wrappedCount;
|
||||
}
|
||||
}
|
||||
|
||||
// Cache lines and heights
|
||||
cachedLines = allLines;
|
||||
cachedLines.swap(allLines);
|
||||
cachedHeights = calculateLineHeights(cachedLines, emotes, isHeader);
|
||||
|
||||
std::vector<MessageBlock> blocks = buildMessageBlocks(isHeader, isMine);
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
#include "CompassRenderer.h"
|
||||
#include "NodeDB.h"
|
||||
#include "NodeListRenderer.h"
|
||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||
#include "modules/StatusMessageModule.h"
|
||||
#endif
|
||||
#include "UIRenderer.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
#include "gps/RTC.h" // for getTime() function
|
||||
@@ -92,8 +95,41 @@ std::string getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node,
|
||||
|
||||
// 1) Choose target candidate (long vs short) only if present
|
||||
const char *raw = nullptr;
|
||||
if (node && node->has_user) {
|
||||
raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name;
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||
// If long-name mode is enabled, and we have a recent status for this node,
|
||||
// prefer "(short_name) statusText" as the raw candidate.
|
||||
std::string composedFromStatus;
|
||||
if (config.display.use_long_node_name && node && node->has_user && statusMessageModule) {
|
||||
const auto &recent = statusMessageModule->getRecentReceived();
|
||||
const StatusMessageModule::RecentStatus *found = nullptr;
|
||||
for (auto it = recent.rbegin(); it != recent.rend(); ++it) {
|
||||
if (it->fromNodeId == node->num && !it->statusText.empty()) {
|
||||
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 += "(";
|
||||
if (shortName && *shortName) {
|
||||
composedFromStatus += shortName;
|
||||
}
|
||||
composedFromStatus += ") ";
|
||||
composedFromStatus += found->statusText;
|
||||
|
||||
raw = composedFromStatus.c_str(); // safe for now; we'll sanitize immediately into std::string
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// If we didn't compose from status, use normal long/short selection
|
||||
if (!raw) {
|
||||
if (node && node->has_user) {
|
||||
raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name;
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Preserve UTF-8 names so emotes can be detected and rendered.
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "NodeListRenderer.h"
|
||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||
#include "modules/StatusMessageModule.h"
|
||||
#endif
|
||||
#include "UIRenderer.h"
|
||||
#include "airtime.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
@@ -290,7 +293,7 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes
|
||||
// * Favorite Node Info *
|
||||
// **********************
|
||||
// cppcheck-suppress constParameterPointer; signature must match FrameCallback typedef from OLEDDisplayUi library
|
||||
void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
if (favoritedNodes.empty())
|
||||
return;
|
||||
@@ -342,6 +345,57 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i
|
||||
UIRenderer::drawStringWithEmotes(display, x, getTextPositions(display)[line++], username, FONT_HEIGHT_SMALL, 1, false);
|
||||
}
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||
// === Optional: Last received StatusMessage line for this node ===
|
||||
// Display it directly under the username line (if we have one).
|
||||
if (statusMessageModule) {
|
||||
const auto &recent = statusMessageModule->getRecentReceived();
|
||||
const StatusMessageModule::RecentStatus *found = nullptr;
|
||||
|
||||
// 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) {
|
||||
std::string statusLine = std::string(" Status: ") + found->statusText;
|
||||
{
|
||||
const int screenW = display->getWidth();
|
||||
const int ellipseW = display->getStringWidth("...");
|
||||
int w = display->getStringWidth(statusLine.c_str());
|
||||
|
||||
// Only do work if it overflows
|
||||
if (w > screenW) {
|
||||
bool truncated = false;
|
||||
if (ellipseW > screenW) {
|
||||
statusLine.clear();
|
||||
} else {
|
||||
while (!statusLine.empty()) {
|
||||
// remove one char (byte) at a time
|
||||
statusLine.pop_back();
|
||||
truncated = true;
|
||||
|
||||
// Measure candidate with ellipsis appended
|
||||
std::string candidate = statusLine + "...";
|
||||
if (display->getStringWidth(candidate.c_str()) <= screenW) {
|
||||
statusLine = std::move(candidate);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (statusLine.empty() && ellipseW <= screenW) {
|
||||
statusLine = "...";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
display->drawString(x, getTextPositions(display)[line++], statusLine.c_str());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// === 2. Signal and Hops (combined on one line, if available) ===
|
||||
char signalHopsStr[32] = "";
|
||||
bool haveSignal = false;
|
||||
|
||||
@@ -50,7 +50,7 @@ class UIRenderer
|
||||
// Navigation bar overlay
|
||||
static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
|
||||
static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
static void drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
|
||||
@@ -121,7 +121,6 @@ void CardputerKeyboard::pressed(uint8_t key)
|
||||
modifierFlag = 0;
|
||||
}
|
||||
|
||||
uint8_t next_key = 0;
|
||||
int row = (key - 1) / 10;
|
||||
int col = (key - 1) % 10;
|
||||
|
||||
|
||||
+23
-5
@@ -10,8 +10,26 @@
|
||||
#include "memGet.h"
|
||||
#include "configuration.h"
|
||||
|
||||
#ifdef ARCH_STM32WL
|
||||
#if defined(MESHTASTIC_DYNAMIC_SBRK_HEAP)
|
||||
#include <malloc.h>
|
||||
#include <unistd.h> // sbrk
|
||||
|
||||
#ifdef ARCH_STM32WL
|
||||
// Returns the uncommitted sbrk headroom: addressable space between the current heap
|
||||
// break and the stack pointer that has not yet been committed to the arena.
|
||||
static uint32_t sbrkHeadroom()
|
||||
{
|
||||
// defined in STM32 linker script
|
||||
extern char _estack;
|
||||
extern char _Min_Stack_Size;
|
||||
|
||||
uint32_t max_sp = (uint32_t)(&_estack - &_Min_Stack_Size);
|
||||
uint32_t heap_end = (uint32_t)sbrk(0);
|
||||
return (max_sp > heap_end) ? (max_sp - heap_end) : 0;
|
||||
}
|
||||
#else
|
||||
#error Unsupported architecture!
|
||||
#endif
|
||||
#endif
|
||||
|
||||
MemGet memGet;
|
||||
@@ -28,9 +46,9 @@ uint32_t MemGet::getFreeHeap()
|
||||
return dbgHeapFree();
|
||||
#elif defined(ARCH_RP2040)
|
||||
return rp2040.getFreeHeap();
|
||||
#elif defined(ARCH_STM32WL)
|
||||
#elif defined(MESHTASTIC_DYNAMIC_SBRK_HEAP) // Currently: ARCH_STM32WL
|
||||
struct mallinfo m = mallinfo();
|
||||
return m.fordblks; // Total free space (bytes)
|
||||
return m.fordblks + sbrkHeadroom(); // Free space within arena + uncommitted sbrk headroom
|
||||
#else
|
||||
// this platform does not have heap management function implemented
|
||||
return UINT32_MAX;
|
||||
@@ -49,9 +67,9 @@ uint32_t MemGet::getHeapSize()
|
||||
return dbgHeapTotal();
|
||||
#elif defined(ARCH_RP2040)
|
||||
return rp2040.getTotalHeap();
|
||||
#elif defined(ARCH_STM32WL)
|
||||
#elif defined(MESHTASTIC_DYNAMIC_SBRK_HEAP) // Currently: ARCH_STM32WL
|
||||
struct mallinfo m = mallinfo();
|
||||
return m.arena; // Non-mmapped space allocated (bytes)
|
||||
return m.arena + sbrkHeadroom(); // Non-mmapped space allocated + uncommitted sbrk headroom
|
||||
#else
|
||||
// this platform does not have heap management function implemented
|
||||
return UINT32_MAX;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <memory>
|
||||
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||
#include "HardwareRNG.h"
|
||||
#include "NodeDB.h"
|
||||
#include "aes-ccm.h"
|
||||
#include "meshUtils.h"
|
||||
@@ -26,6 +27,15 @@ void CryptoEngine::generateKeyPair(uint8_t *pubKey, uint8_t *privKey)
|
||||
{
|
||||
// Mix in any randomness we can, to make key generation stronger.
|
||||
CryptRNG.begin(optstr(APP_VERSION));
|
||||
|
||||
uint8_t hardwareEntropy[64] = {0};
|
||||
if (HardwareRNG::fill(hardwareEntropy, sizeof(hardwareEntropy), true)) {
|
||||
CryptRNG.stir(hardwareEntropy, sizeof(hardwareEntropy));
|
||||
} else {
|
||||
LOG_WARN("Hardware entropy unavailable, falling back to software RNG");
|
||||
}
|
||||
memset(hardwareEntropy, 0, sizeof(hardwareEntropy));
|
||||
|
||||
if (myNodeInfo.device_id.size == 16) {
|
||||
CryptRNG.stir(myNodeInfo.device_id.bytes, myNodeInfo.device_id.size);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
#include "HardwareRNG.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <random>
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#if HAS_RADIO
|
||||
#include "RadioLibInterface.h"
|
||||
#endif
|
||||
|
||||
#if defined(ARCH_NRF52)
|
||||
#include <Adafruit_nRFCrypto.h>
|
||||
extern Adafruit_nRFCrypto nRFCrypto;
|
||||
#elif defined(ARCH_ESP32)
|
||||
#include <esp_system.h>
|
||||
#elif defined(ARCH_RP2040)
|
||||
#include <Arduino.h>
|
||||
#elif defined(ARCH_PORTDUINO)
|
||||
#include <random>
|
||||
#include <sys/random.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace HardwareRNG
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
void fillWithRandomDevice(uint8_t *buffer, size_t length)
|
||||
{
|
||||
std::random_device rd;
|
||||
size_t offset = 0;
|
||||
while (offset < length) {
|
||||
uint32_t value = rd();
|
||||
size_t toCopy = std::min(length - offset, sizeof(value));
|
||||
memcpy(buffer + offset, &value, toCopy);
|
||||
offset += toCopy;
|
||||
}
|
||||
}
|
||||
|
||||
#if HAS_RADIO
|
||||
bool mixWithLoRaEntropy(uint8_t *buffer, size_t length)
|
||||
{
|
||||
// Only attempt to pull entropy from the modem if it is initialized and exposes the helper.
|
||||
// When the radio stack is disabled or has not yet been configured, we simply skip this step
|
||||
// and return false so callers know no extra mixing occurred.
|
||||
RadioLibInterface *radio = RadioLibInterface::instance;
|
||||
if (!radio) {
|
||||
LOG_ERROR("No radio instance available to provide entropy");
|
||||
return false;
|
||||
}
|
||||
|
||||
constexpr size_t chunkSize = 16;
|
||||
uint8_t scratch[chunkSize];
|
||||
size_t offset = 0;
|
||||
bool mixed = false;
|
||||
|
||||
while (offset < length) {
|
||||
size_t toCopy = std::min(length - offset, chunkSize);
|
||||
|
||||
// randomBytes() returns false if the modem does not support it or is not ready
|
||||
// (for instance, when the radio is powered down). We break immediately to avoid
|
||||
// blocking or returning partially-filled entropy and simply report failure.
|
||||
if (!radio->randomBytes(scratch, toCopy)) {
|
||||
break;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < toCopy; ++i) {
|
||||
buffer[offset + i] ^= scratch[i];
|
||||
}
|
||||
|
||||
mixed = true;
|
||||
offset += toCopy;
|
||||
}
|
||||
|
||||
// Avoid leaving the modem-sourced bytes sitting on the stack longer than needed.
|
||||
if (mixed) {
|
||||
memset(scratch, 0, sizeof(scratch));
|
||||
}
|
||||
|
||||
return mixed;
|
||||
}
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
bool fill(uint8_t *buffer, size_t length, bool useRadioEntropy)
|
||||
{
|
||||
if (!buffer || length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool filled = false;
|
||||
|
||||
#if defined(ARCH_NRF52)
|
||||
// The Nordic SDK RNG provides cryptographic-quality randomness backed by hardware.
|
||||
nRFCrypto.begin();
|
||||
auto result = nRFCrypto.Random.generate(buffer, length);
|
||||
nRFCrypto.end();
|
||||
filled = result;
|
||||
#elif defined(ARCH_ESP32)
|
||||
// ESP32 exposes a true RNG via esp_fill_random().
|
||||
esp_fill_random(buffer, length);
|
||||
filled = true;
|
||||
#elif defined(ARCH_RP2040)
|
||||
// RP2040 has a hardware random number generator accessible through the Arduino core.
|
||||
size_t offset = 0;
|
||||
while (offset < length) {
|
||||
uint32_t value = rp2040.hwrand32();
|
||||
size_t toCopy = std::min(length - offset, sizeof(value));
|
||||
memcpy(buffer + offset, &value, toCopy);
|
||||
offset += toCopy;
|
||||
}
|
||||
filled = true;
|
||||
#elif defined(ARCH_PORTDUINO)
|
||||
// Prefer the host OS RNG first when running under Portduino.
|
||||
ssize_t generated = ::getrandom(buffer, length, 0);
|
||||
if (generated == static_cast<ssize_t>(length)) {
|
||||
filled = true;
|
||||
}
|
||||
|
||||
if (!filled) {
|
||||
fillWithRandomDevice(buffer, length);
|
||||
filled = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!filled) {
|
||||
// As a last resort, fall back to std::random_device. This should only be reached
|
||||
// if a platform-specific source was unavailable.
|
||||
fillWithRandomDevice(buffer, length);
|
||||
filled = true;
|
||||
}
|
||||
|
||||
#if HAS_RADIO
|
||||
if (useRadioEntropy) {
|
||||
// Best-effort: if the radio is active and can provide modem entropy, XOR it over the
|
||||
// buffer to improve overall quality. We consider the filling a success if either a
|
||||
// good platform RNG or the modem RNG provided data, so we return true as long as at
|
||||
// least one of those steps succeeded.
|
||||
filled = mixWithLoRaEntropy(buffer, length) || filled;
|
||||
}
|
||||
#endif
|
||||
|
||||
return filled;
|
||||
}
|
||||
|
||||
bool seed(uint32_t &seedOut)
|
||||
{
|
||||
uint32_t candidate = 0;
|
||||
if (!fill(reinterpret_cast<uint8_t *>(&candidate), sizeof(candidate), true)) {
|
||||
return false;
|
||||
}
|
||||
seedOut = candidate;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace HardwareRNG
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace HardwareRNG
|
||||
{
|
||||
|
||||
/**
|
||||
* Fill the provided buffer with random bytes sourced from the most
|
||||
* appropriate hardware-backed RNG available on the current platform.
|
||||
*
|
||||
* @param buffer Destination buffer for random bytes
|
||||
* @param length Number of bytes to write
|
||||
* @param useRadioEntropy If true, attempt to mix radio entropy into the output as well.
|
||||
* @return true if the buffer was fully populated with entropy, false on failure
|
||||
*/
|
||||
bool fill(uint8_t *buffer, size_t length, bool useRadioEntropy = false);
|
||||
|
||||
/**
|
||||
* Populate a 32-bit seed value with hardware-backed randomness where possible.
|
||||
*
|
||||
* @param seedOut Destination for the generated seed value
|
||||
* @return true if a seed was produced from a reliable entropy source
|
||||
*/
|
||||
bool seed(uint32_t &seedOut);
|
||||
|
||||
} // namespace HardwareRNG
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "MeshTypes.h"
|
||||
#include "PointerQueue.h"
|
||||
#include "configuration.h"
|
||||
#include "detect/LoRaRadioType.h"
|
||||
|
||||
// Sentinel marking the end of a modem preset array
|
||||
static constexpr meshtastic_Config_LoRaConfig_ModemPreset MODEM_PRESET_END =
|
||||
@@ -59,7 +60,7 @@ extern const RegionInfo *myRegion;
|
||||
extern void initRegion();
|
||||
|
||||
// Valid LoRa spread factor range and defaults
|
||||
constexpr uint8_t LORA_SF_MIN = 7;
|
||||
constexpr uint8_t LORA_SF_MIN = 5;
|
||||
constexpr uint8_t LORA_SF_MAX = 12;
|
||||
constexpr uint8_t LORA_SF_DEFAULT = 11; // LONG_FAST default
|
||||
|
||||
@@ -71,10 +72,14 @@ constexpr uint8_t LORA_CR_DEFAULT = 5; // LONG_FAST default
|
||||
// Default bandwidth in kHz (LONG_FAST)
|
||||
constexpr float LORA_BW_DEFAULT_KHZ = 250.0f;
|
||||
|
||||
/// Clamp spread factor to the valid LoRa range [7, 12].
|
||||
/// Clamp spread factor to the valid LoRa range [5, 12].
|
||||
/// Out-of-range values (including 0 from unset preset mode) return LORA_SF_DEFAULT.
|
||||
static inline uint8_t clampSpreadFactor(uint8_t sf)
|
||||
{
|
||||
// We check for RF95 radios that are incompatible with Spreading Factors 5 and 6.
|
||||
if (radioType == RF95_RADIO && (sf == 5 || sf == 6))
|
||||
return LORA_SF_DEFAULT;
|
||||
|
||||
if (sf < LORA_SF_MIN || sf > LORA_SF_MAX)
|
||||
return LORA_SF_DEFAULT;
|
||||
return sf;
|
||||
|
||||
@@ -246,6 +246,24 @@ bool RadioLibInterface::findInTxQueue(NodeNum from, PacketId id)
|
||||
return txQueue.find(from, id);
|
||||
}
|
||||
|
||||
bool RadioLibInterface::randomBytes(uint8_t *buffer, size_t length)
|
||||
{
|
||||
if (!buffer || length == 0 || !iface) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Older RadioLib versions only expose random(min, max), so fill the buffer byte-by-byte.
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
int32_t value = iface->random(0, 255);
|
||||
if (value < 0) {
|
||||
return false;
|
||||
}
|
||||
buffer[i] = static_cast<uint8_t>(value & 0xFF);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** radio helper thread callback.
|
||||
We never immediately transmit after any operation (either Rx or Tx). Instead we should wait a random multiple of
|
||||
'slotTimes' (see definition in RadioInterface.h) taken from a contention window (CW) to lower the chance of collision.
|
||||
@@ -587,4 +605,4 @@ bool RadioLibInterface::startSend(meshtastic_MeshPacket *txp)
|
||||
|
||||
return res == RADIOLIB_ERR_NONE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,6 +172,12 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
|
||||
/** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */
|
||||
virtual bool findInTxQueue(NodeNum from, PacketId id) override;
|
||||
|
||||
/**
|
||||
* Request randomness sourced from the LoRa modem, if supported by the active RadioLib interface.
|
||||
* @return true if len bytes were produced, false otherwise.
|
||||
*/
|
||||
bool randomBytes(uint8_t *buffer, size_t length);
|
||||
|
||||
private:
|
||||
/** if we have something waiting to send, start a short (random) timer so we can come check for collision before actually
|
||||
* doing the transmit */
|
||||
|
||||
+2
-10
@@ -21,8 +21,6 @@
|
||||
#if ARCH_PORTDUINO
|
||||
#include "Throttle.h"
|
||||
#include "platform/portduino/PortduinoGlue.h"
|
||||
#endif
|
||||
#if ENABLE_JSON_LOGGING || ARCH_PORTDUINO
|
||||
#include "serialization/MeshPacketSerializer.h"
|
||||
#endif
|
||||
|
||||
@@ -544,9 +542,7 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
} */
|
||||
|
||||
printPacket("decoded message", p);
|
||||
#if ENABLE_JSON_LOGGING
|
||||
LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str());
|
||||
#elif ARCH_PORTDUINO
|
||||
#if ARCH_PORTDUINO
|
||||
if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) {
|
||||
LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str());
|
||||
} else if (portduino_config.JSONFilename != "") {
|
||||
@@ -838,11 +834,7 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
|
||||
|
||||
void Router::perhapsHandleReceived(meshtastic_MeshPacket *p)
|
||||
{
|
||||
#if ENABLE_JSON_LOGGING
|
||||
// Even ignored packets get logged in the trace
|
||||
p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone
|
||||
LOG_TRACE("%s", MeshPacketSerializer::JsonSerializeEncrypted(p).c_str());
|
||||
#elif ARCH_PORTDUINO
|
||||
#if ARCH_PORTDUINO
|
||||
// Even ignored packets get logged in the trace
|
||||
if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) {
|
||||
p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone
|
||||
|
||||
@@ -27,6 +27,42 @@ PB_BIND(meshtastic_PLI, meshtastic_PLI, AUTO)
|
||||
PB_BIND(meshtastic_AircraftTrack, meshtastic_AircraftTrack, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_CotGeoPoint, meshtastic_CotGeoPoint, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_DrawnShape, meshtastic_DrawnShape, 2)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_Marker, meshtastic_Marker, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_RangeAndBearing, meshtastic_RangeAndBearing, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_Route, meshtastic_Route, 2)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_Route_Link, meshtastic_Route_Link, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_CasevacReport, meshtastic_CasevacReport, 2)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_ZMistEntry, meshtastic_ZMistEntry, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_EmergencyAlert, meshtastic_EmergencyAlert, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_TaskRequest, meshtastic_TaskRequest, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_TAKEnvironment, meshtastic_TAKEnvironment, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_SensorFov, meshtastic_SensorFov, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_TAKPacketV2, meshtastic_TAKPacketV2, 2)
|
||||
|
||||
|
||||
@@ -41,3 +77,29 @@ PB_BIND(meshtastic_TAKPacketV2, meshtastic_TAKPacketV2, 2)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,9 @@ PB_BIND(meshtastic_KeyVerification, meshtastic_KeyVerification, AUTO)
|
||||
PB_BIND(meshtastic_StoreForwardPlusPlus, meshtastic_StoreForwardPlusPlus, 2)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_RemoteShell, meshtastic_RemoteShell, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_Waypoint, meshtastic_Waypoint, AUTO)
|
||||
|
||||
|
||||
@@ -129,6 +132,8 @@ PB_BIND(meshtastic_ChunkedPayloadResponse, meshtastic_ChunkedPayloadResponse, AU
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -308,8 +308,13 @@ typedef enum _meshtastic_HardwareModel {
|
||||
meshtastic_HardwareModel_TDISPLAY_S3_PRO = 126,
|
||||
/* Heltec Mesh Node T096 board features an nRF52840 CPU and a TFT screen. */
|
||||
meshtastic_HardwareModel_HELTEC_MESH_NODE_T096 = 127,
|
||||
/* Seeed studio T1000-E Pro tracker card. NRF52840 w/ LR2021 radio, GPS, button, buzzer, and sensors. */
|
||||
/* Seeed studio T1000-E Pro tracker card. NRF52840 w/ LR2021 radio,
|
||||
GPS, button, buzzer, and sensors. */
|
||||
meshtastic_HardwareModel_TRACKER_T1000_E_PRO = 128,
|
||||
/* Elecrow ThinkNode M7, M8 and M9 */
|
||||
meshtastic_HardwareModel_THINKNODE_M7 = 129,
|
||||
meshtastic_HardwareModel_THINKNODE_M8 = 130,
|
||||
meshtastic_HardwareModel_THINKNODE_M9 = 131,
|
||||
/* ------------------------------------------------------------------------------------------------------------------------------------------
|
||||
Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits.
|
||||
------------------------------------------------------------------------------------------------------------------------------------------ */
|
||||
@@ -513,6 +518,27 @@ typedef enum _meshtastic_StoreForwardPlusPlus_SFPP_message_type {
|
||||
meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF = 6
|
||||
} meshtastic_StoreForwardPlusPlus_SFPP_message_type;
|
||||
|
||||
/* Frame op code for PTY session control and stream transport.
|
||||
|
||||
Values 1-63 are client->server requests.
|
||||
Values 64-127 are server->client responses/events. */
|
||||
typedef enum _meshtastic_RemoteShell_OpCode {
|
||||
meshtastic_RemoteShell_OpCode_OP_UNSET = 0,
|
||||
/* Client -> server */
|
||||
meshtastic_RemoteShell_OpCode_OPEN = 1,
|
||||
meshtastic_RemoteShell_OpCode_INPUT = 2,
|
||||
meshtastic_RemoteShell_OpCode_RESIZE = 3,
|
||||
meshtastic_RemoteShell_OpCode_CLOSE = 4,
|
||||
meshtastic_RemoteShell_OpCode_PING = 5,
|
||||
meshtastic_RemoteShell_OpCode_ACK = 6,
|
||||
/* Server -> client */
|
||||
meshtastic_RemoteShell_OpCode_OPEN_OK = 64,
|
||||
meshtastic_RemoteShell_OpCode_OUTPUT = 65,
|
||||
meshtastic_RemoteShell_OpCode_CLOSED = 66,
|
||||
meshtastic_RemoteShell_OpCode_ERROR = 67,
|
||||
meshtastic_RemoteShell_OpCode_PONG = 68
|
||||
} meshtastic_RemoteShell_OpCode;
|
||||
|
||||
/* The priority of this message for sending.
|
||||
Higher priorities are sent first (when managing the transmit queue).
|
||||
This field is never sent over the air, it is only used internally inside of a local device node.
|
||||
@@ -845,6 +871,31 @@ typedef struct _meshtastic_StoreForwardPlusPlus {
|
||||
uint32_t chain_count;
|
||||
} meshtastic_StoreForwardPlusPlus;
|
||||
|
||||
typedef PB_BYTES_ARRAY_T(200) meshtastic_RemoteShell_payload_t;
|
||||
/* The actual over-the-mesh message doing RemoteShell */
|
||||
typedef struct _meshtastic_RemoteShell {
|
||||
/* Structured frame operation. */
|
||||
meshtastic_RemoteShell_OpCode op;
|
||||
/* Logical PTY session identifier. */
|
||||
uint32_t session_id;
|
||||
/* Monotonic sequence number for this frame. */
|
||||
uint32_t seq;
|
||||
/* Cumulative ack sequence number. */
|
||||
uint32_t ack_seq;
|
||||
/* Opaque bytes payload for INPUT/OUTPUT/ERROR and other frame bodies. */
|
||||
meshtastic_RemoteShell_payload_t payload;
|
||||
/* Terminal size columns used for OPEN/RESIZE signaling. */
|
||||
uint32_t cols;
|
||||
/* Terminal size rows used for OPEN/RESIZE signaling. */
|
||||
uint32_t rows;
|
||||
/* Bit flags for protocol extensions. */
|
||||
uint32_t flags;
|
||||
/* The last sequence number TX'd. */
|
||||
uint32_t last_tx_seq;
|
||||
/* The last sequence number RX'd. */
|
||||
uint32_t last_rx_seq;
|
||||
} meshtastic_RemoteShell;
|
||||
|
||||
/* Waypoint message, used to share arbitrary locations across the mesh */
|
||||
typedef struct _meshtastic_Waypoint {
|
||||
/* Id of the waypoint */
|
||||
@@ -1385,6 +1436,10 @@ extern "C" {
|
||||
#define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_MAX meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF
|
||||
#define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_ARRAYSIZE ((meshtastic_StoreForwardPlusPlus_SFPP_message_type)(meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF+1))
|
||||
|
||||
#define _meshtastic_RemoteShell_OpCode_MIN meshtastic_RemoteShell_OpCode_OP_UNSET
|
||||
#define _meshtastic_RemoteShell_OpCode_MAX meshtastic_RemoteShell_OpCode_PONG
|
||||
#define _meshtastic_RemoteShell_OpCode_ARRAYSIZE ((meshtastic_RemoteShell_OpCode)(meshtastic_RemoteShell_OpCode_PONG+1))
|
||||
|
||||
#define _meshtastic_MeshPacket_Priority_MIN meshtastic_MeshPacket_Priority_UNSET
|
||||
#define _meshtastic_MeshPacket_Priority_MAX meshtastic_MeshPacket_Priority_MAX
|
||||
#define _meshtastic_MeshPacket_Priority_ARRAYSIZE ((meshtastic_MeshPacket_Priority)(meshtastic_MeshPacket_Priority_MAX+1))
|
||||
@@ -1415,6 +1470,8 @@ extern "C" {
|
||||
|
||||
#define meshtastic_StoreForwardPlusPlus_sfpp_message_type_ENUMTYPE meshtastic_StoreForwardPlusPlus_SFPP_message_type
|
||||
|
||||
#define meshtastic_RemoteShell_op_ENUMTYPE meshtastic_RemoteShell_OpCode
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1459,6 +1516,7 @@ extern "C" {
|
||||
#define meshtastic_Data_init_default {_meshtastic_PortNum_MIN, {0, {0}}, 0, 0, 0, 0, 0, 0, false, 0}
|
||||
#define meshtastic_KeyVerification_init_default {0, {0, {0}}, {0, {0}}}
|
||||
#define meshtastic_StoreForwardPlusPlus_init_default {_meshtastic_StoreForwardPlusPlus_SFPP_message_type_MIN, {0, {0}}, {0, {0}}, {0, {0}}, {0, {0}}, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_RemoteShell_init_default {_meshtastic_RemoteShell_OpCode_MIN, 0, 0, 0, {0, {0}}, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_Waypoint_init_default {0, false, 0, false, 0, 0, 0, "", "", 0}
|
||||
#define meshtastic_StatusMessage_init_default {""}
|
||||
#define meshtastic_MqttClientProxyMessage_init_default {"", 0, {{0, {0}}}, 0}
|
||||
@@ -1492,6 +1550,7 @@ extern "C" {
|
||||
#define meshtastic_Data_init_zero {_meshtastic_PortNum_MIN, {0, {0}}, 0, 0, 0, 0, 0, 0, false, 0}
|
||||
#define meshtastic_KeyVerification_init_zero {0, {0, {0}}, {0, {0}}}
|
||||
#define meshtastic_StoreForwardPlusPlus_init_zero {_meshtastic_StoreForwardPlusPlus_SFPP_message_type_MIN, {0, {0}}, {0, {0}}, {0, {0}}, {0, {0}}, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_RemoteShell_init_zero {_meshtastic_RemoteShell_OpCode_MIN, 0, 0, 0, {0, {0}}, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_Waypoint_init_zero {0, false, 0, false, 0, 0, 0, "", "", 0}
|
||||
#define meshtastic_StatusMessage_init_zero {""}
|
||||
#define meshtastic_MqttClientProxyMessage_init_zero {"", 0, {{0, {0}}}, 0}
|
||||
@@ -1581,6 +1640,16 @@ extern "C" {
|
||||
#define meshtastic_StoreForwardPlusPlus_encapsulated_from_tag 8
|
||||
#define meshtastic_StoreForwardPlusPlus_encapsulated_rxtime_tag 9
|
||||
#define meshtastic_StoreForwardPlusPlus_chain_count_tag 10
|
||||
#define meshtastic_RemoteShell_op_tag 1
|
||||
#define meshtastic_RemoteShell_session_id_tag 2
|
||||
#define meshtastic_RemoteShell_seq_tag 3
|
||||
#define meshtastic_RemoteShell_ack_seq_tag 4
|
||||
#define meshtastic_RemoteShell_payload_tag 5
|
||||
#define meshtastic_RemoteShell_cols_tag 6
|
||||
#define meshtastic_RemoteShell_rows_tag 7
|
||||
#define meshtastic_RemoteShell_flags_tag 8
|
||||
#define meshtastic_RemoteShell_last_tx_seq_tag 9
|
||||
#define meshtastic_RemoteShell_last_rx_seq_tag 10
|
||||
#define meshtastic_Waypoint_id_tag 1
|
||||
#define meshtastic_Waypoint_latitude_i_tag 2
|
||||
#define meshtastic_Waypoint_longitude_i_tag 3
|
||||
@@ -1813,6 +1882,20 @@ X(a, STATIC, SINGULAR, UINT32, chain_count, 10)
|
||||
#define meshtastic_StoreForwardPlusPlus_CALLBACK NULL
|
||||
#define meshtastic_StoreForwardPlusPlus_DEFAULT NULL
|
||||
|
||||
#define meshtastic_RemoteShell_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UENUM, op, 1) \
|
||||
X(a, STATIC, SINGULAR, UINT32, session_id, 2) \
|
||||
X(a, STATIC, SINGULAR, UINT32, seq, 3) \
|
||||
X(a, STATIC, SINGULAR, UINT32, ack_seq, 4) \
|
||||
X(a, STATIC, SINGULAR, BYTES, payload, 5) \
|
||||
X(a, STATIC, SINGULAR, UINT32, cols, 6) \
|
||||
X(a, STATIC, SINGULAR, UINT32, rows, 7) \
|
||||
X(a, STATIC, SINGULAR, UINT32, flags, 8) \
|
||||
X(a, STATIC, SINGULAR, UINT32, last_tx_seq, 9) \
|
||||
X(a, STATIC, SINGULAR, UINT32, last_rx_seq, 10)
|
||||
#define meshtastic_RemoteShell_CALLBACK NULL
|
||||
#define meshtastic_RemoteShell_DEFAULT NULL
|
||||
|
||||
#define meshtastic_Waypoint_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UINT32, id, 1) \
|
||||
X(a, STATIC, OPTIONAL, SFIXED32, latitude_i, 2) \
|
||||
@@ -2095,6 +2178,7 @@ extern const pb_msgdesc_t meshtastic_Routing_msg;
|
||||
extern const pb_msgdesc_t meshtastic_Data_msg;
|
||||
extern const pb_msgdesc_t meshtastic_KeyVerification_msg;
|
||||
extern const pb_msgdesc_t meshtastic_StoreForwardPlusPlus_msg;
|
||||
extern const pb_msgdesc_t meshtastic_RemoteShell_msg;
|
||||
extern const pb_msgdesc_t meshtastic_Waypoint_msg;
|
||||
extern const pb_msgdesc_t meshtastic_StatusMessage_msg;
|
||||
extern const pb_msgdesc_t meshtastic_MqttClientProxyMessage_msg;
|
||||
@@ -2130,6 +2214,7 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg;
|
||||
#define meshtastic_Data_fields &meshtastic_Data_msg
|
||||
#define meshtastic_KeyVerification_fields &meshtastic_KeyVerification_msg
|
||||
#define meshtastic_StoreForwardPlusPlus_fields &meshtastic_StoreForwardPlusPlus_msg
|
||||
#define meshtastic_RemoteShell_fields &meshtastic_RemoteShell_msg
|
||||
#define meshtastic_Waypoint_fields &meshtastic_Waypoint_msg
|
||||
#define meshtastic_StatusMessage_fields &meshtastic_StatusMessage_msg
|
||||
#define meshtastic_MqttClientProxyMessage_fields &meshtastic_MqttClientProxyMessage_msg
|
||||
@@ -2185,6 +2270,7 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg;
|
||||
#define meshtastic_NodeRemoteHardwarePin_size 29
|
||||
#define meshtastic_Position_size 144
|
||||
#define meshtastic_QueueStatus_size 23
|
||||
#define meshtastic_RemoteShell_size 253
|
||||
#define meshtastic_RouteDiscovery_size 256
|
||||
#define meshtastic_Routing_size 259
|
||||
#define meshtastic_StatusMessage_size 81
|
||||
|
||||
@@ -76,6 +76,8 @@ typedef enum _meshtastic_PortNum {
|
||||
meshtastic_PortNum_ALERT_APP = 11,
|
||||
/* Module/port for handling key verification requests. */
|
||||
meshtastic_PortNum_KEY_VERIFICATION_APP = 12,
|
||||
/* Module/port for handling primitive remote shell access. */
|
||||
meshtastic_PortNum_REMOTE_SHELL_APP = 13,
|
||||
/* Provides a 'ping' service that replies to any packet it receives.
|
||||
Also serves as a small example module.
|
||||
ENCODING: ASCII Plaintext */
|
||||
|
||||
@@ -113,7 +113,9 @@ typedef enum _meshtastic_TelemetrySensorType {
|
||||
/* SCD30 CO2, humidity, temperature sensor */
|
||||
meshtastic_TelemetrySensorType_SCD30 = 49,
|
||||
/* SHT family of sensors for temperature and humidity */
|
||||
meshtastic_TelemetrySensorType_SHTXX = 50
|
||||
meshtastic_TelemetrySensorType_SHTXX = 50,
|
||||
/* DS248X Bridge for one-wire temperature sensors */
|
||||
meshtastic_TelemetrySensorType_DS248X = 51
|
||||
} meshtastic_TelemetrySensorType;
|
||||
|
||||
/* Struct definitions */
|
||||
@@ -206,6 +208,9 @@ typedef struct _meshtastic_EnvironmentMetrics {
|
||||
/* Soil temperature measured (*C) */
|
||||
bool has_soil_temperature;
|
||||
float soil_temperature;
|
||||
/* One-wire temperature (*C) */
|
||||
pb_size_t one_wire_temperature_count;
|
||||
float one_wire_temperature[8];
|
||||
} meshtastic_EnvironmentMetrics;
|
||||
|
||||
/* Power Metrics (voltage / current / etc) */
|
||||
@@ -491,8 +496,8 @@ extern "C" {
|
||||
|
||||
/* Helper constants for enums */
|
||||
#define _meshtastic_TelemetrySensorType_MIN meshtastic_TelemetrySensorType_SENSOR_UNSET
|
||||
#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_SHTXX
|
||||
#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_SHTXX+1))
|
||||
#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_DS248X
|
||||
#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_DS248X+1))
|
||||
|
||||
|
||||
|
||||
@@ -508,7 +513,7 @@ extern "C" {
|
||||
|
||||
/* Initializer values for message structs */
|
||||
#define meshtastic_DeviceMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||
#define meshtastic_EnvironmentMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||
#define meshtastic_EnvironmentMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, 0, {0, 0, 0, 0, 0, 0, 0, 0}}
|
||||
#define meshtastic_PowerMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||
#define meshtastic_AirQualityMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||
#define meshtastic_LocalStats_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
@@ -519,7 +524,7 @@ extern "C" {
|
||||
#define meshtastic_Nau7802Config_init_default {0, 0}
|
||||
#define meshtastic_SEN5XState_init_default {0, 0, 0, false, 0, false, 0, false, 0}
|
||||
#define meshtastic_DeviceMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||
#define meshtastic_EnvironmentMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||
#define meshtastic_EnvironmentMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, 0, {0, 0, 0, 0, 0, 0, 0, 0}}
|
||||
#define meshtastic_PowerMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||
#define meshtastic_AirQualityMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||
#define meshtastic_LocalStats_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
@@ -558,6 +563,7 @@ extern "C" {
|
||||
#define meshtastic_EnvironmentMetrics_rainfall_24h_tag 20
|
||||
#define meshtastic_EnvironmentMetrics_soil_moisture_tag 21
|
||||
#define meshtastic_EnvironmentMetrics_soil_temperature_tag 22
|
||||
#define meshtastic_EnvironmentMetrics_one_wire_temperature_tag 23
|
||||
#define meshtastic_PowerMetrics_ch1_voltage_tag 1
|
||||
#define meshtastic_PowerMetrics_ch1_current_tag 2
|
||||
#define meshtastic_PowerMetrics_ch2_voltage_tag 3
|
||||
@@ -683,7 +689,8 @@ X(a, STATIC, OPTIONAL, FLOAT, radiation, 18) \
|
||||
X(a, STATIC, OPTIONAL, FLOAT, rainfall_1h, 19) \
|
||||
X(a, STATIC, OPTIONAL, FLOAT, rainfall_24h, 20) \
|
||||
X(a, STATIC, OPTIONAL, UINT32, soil_moisture, 21) \
|
||||
X(a, STATIC, OPTIONAL, FLOAT, soil_temperature, 22)
|
||||
X(a, STATIC, OPTIONAL, FLOAT, soil_temperature, 22) \
|
||||
X(a, STATIC, REPEATED, FLOAT, one_wire_temperature, 23)
|
||||
#define meshtastic_EnvironmentMetrics_CALLBACK NULL
|
||||
#define meshtastic_EnvironmentMetrics_DEFAULT NULL
|
||||
|
||||
@@ -852,7 +859,7 @@ extern const pb_msgdesc_t meshtastic_SEN5XState_msg;
|
||||
#define MESHTASTIC_MESHTASTIC_TELEMETRY_PB_H_MAX_SIZE meshtastic_Telemetry_size
|
||||
#define meshtastic_AirQualityMetrics_size 150
|
||||
#define meshtastic_DeviceMetrics_size 27
|
||||
#define meshtastic_EnvironmentMetrics_size 113
|
||||
#define meshtastic_EnvironmentMetrics_size 161
|
||||
#define meshtastic_HealthMetrics_size 11
|
||||
#define meshtastic_HostMetrics_size 264
|
||||
#define meshtastic_LocalStats_size 87
|
||||
|
||||
+255
-177
@@ -11,11 +11,12 @@
|
||||
#endif
|
||||
#include "SPILock.h"
|
||||
#include "power.h"
|
||||
#include "serialization/JSON.h"
|
||||
#include <FSCommon.h>
|
||||
#include <HTTPBodyParser.hpp>
|
||||
#include <HTTPMultipartBodyParser.hpp>
|
||||
#include <HTTPURLEncodedBodyParser.hpp>
|
||||
#include <cmath>
|
||||
#include <sstream>
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
#include "esp_task_wdt.h"
|
||||
@@ -259,40 +260,95 @@ void htmlDeleteDir(const char *dirname)
|
||||
root.close();
|
||||
}
|
||||
|
||||
JSONArray htmlListDir(const char *dirname, uint8_t levels)
|
||||
// Escape a string into a JSON double-quoted literal. Matches the previous
|
||||
// SimpleJSON StringifyString behavior (0x00-0x1F and 0x7F -> \u00xx lowercase,
|
||||
// escapes " \ / \b \f \n \r \t, UTF-8 passes through unchanged).
|
||||
static std::string jsonEscape(const std::string &str)
|
||||
{
|
||||
std::string out = "\"";
|
||||
for (size_t i = 0; i < str.size(); ++i) {
|
||||
char chr = str[i];
|
||||
if (chr == '"' || chr == '\\' || chr == '/') {
|
||||
out += '\\';
|
||||
out += chr;
|
||||
} else if (chr == '\b') {
|
||||
out += "\\b";
|
||||
} else if (chr == '\f') {
|
||||
out += "\\f";
|
||||
} else if (chr == '\n') {
|
||||
out += "\\n";
|
||||
} else if (chr == '\r') {
|
||||
out += "\\r";
|
||||
} else if (chr == '\t') {
|
||||
out += "\\t";
|
||||
} else if ((unsigned char)chr < 0x20 || chr == 0x7F) {
|
||||
char buf[8];
|
||||
snprintf(buf, sizeof(buf), "\\u%04x", (unsigned char)chr);
|
||||
out += buf;
|
||||
} else {
|
||||
out += chr;
|
||||
}
|
||||
}
|
||||
out += "\"";
|
||||
return out;
|
||||
}
|
||||
|
||||
// Format a numeric value the way the previous SimpleJSON serializer did
|
||||
// (std::stringstream with precision 15, NaN/Inf -> "null").
|
||||
static std::string jsonNum(double v)
|
||||
{
|
||||
if (std::isinf(v) || std::isnan(v))
|
||||
return "null";
|
||||
std::ostringstream ss;
|
||||
ss.precision(15);
|
||||
ss << v;
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// Build a serialized JSON array string listing files in `dirname`.
|
||||
// Subdirectories recurse as nested arrays (up to `levels` deep).
|
||||
std::string htmlListDir(const char *dirname, uint8_t levels)
|
||||
{
|
||||
File root = FSCom.open(dirname, FILE_O_READ);
|
||||
JSONArray fileList;
|
||||
std::string out = "[";
|
||||
bool first = true;
|
||||
if (!root) {
|
||||
return fileList;
|
||||
out += "]";
|
||||
return out;
|
||||
}
|
||||
if (!root.isDirectory()) {
|
||||
return fileList;
|
||||
out += "]";
|
||||
return out;
|
||||
}
|
||||
|
||||
// iterate over the file list
|
||||
File file = root.openNextFile();
|
||||
while (file) {
|
||||
std::string element;
|
||||
bool haveElement = false;
|
||||
if (file.isDirectory() && !String(file.name()).endsWith(".")) {
|
||||
if (levels) {
|
||||
#ifdef ARCH_ESP32
|
||||
fileList.push_back(new JSONValue(htmlListDir(file.path(), levels - 1)));
|
||||
element = htmlListDir(file.path(), levels - 1);
|
||||
#else
|
||||
fileList.push_back(new JSONValue(htmlListDir(file.name(), levels - 1)));
|
||||
element = htmlListDir(file.name(), levels - 1);
|
||||
#endif
|
||||
haveElement = true;
|
||||
file.close();
|
||||
}
|
||||
} else {
|
||||
JSONObject thisFileMap;
|
||||
thisFileMap["size"] = new JSONValue((int)file.size());
|
||||
#ifdef ARCH_ESP32
|
||||
String fileName = String(file.path()).substring(1);
|
||||
thisFileMap["name"] = new JSONValue(fileName.c_str());
|
||||
#else
|
||||
String fileName = String(file.name()).substring(1);
|
||||
thisFileMap["name"] = new JSONValue(fileName.c_str());
|
||||
#endif
|
||||
String tempName = String(file.name()).substring(1);
|
||||
// Keys in the previous std::map<string,...> were emitted in
|
||||
// alphabetical order: name, nameModified, size.
|
||||
element = "{";
|
||||
element += jsonEscape("name");
|
||||
element += ":";
|
||||
element += jsonEscape(fileName.c_str());
|
||||
if (tempName.endsWith(".gz")) {
|
||||
#ifdef ARCH_ESP32
|
||||
String modifiedFile = String(file.path()).substring(1);
|
||||
@@ -300,15 +356,30 @@ JSONArray htmlListDir(const char *dirname, uint8_t levels)
|
||||
String modifiedFile = String(file.name()).substring(1);
|
||||
#endif
|
||||
modifiedFile.remove((modifiedFile.length() - 3), 3);
|
||||
thisFileMap["nameModified"] = new JSONValue(modifiedFile.c_str());
|
||||
element += ",";
|
||||
element += jsonEscape("nameModified");
|
||||
element += ":";
|
||||
element += jsonEscape(modifiedFile.c_str());
|
||||
}
|
||||
fileList.push_back(new JSONValue(thisFileMap));
|
||||
element += ",";
|
||||
element += jsonEscape("size");
|
||||
element += ":";
|
||||
element += jsonNum((int)file.size());
|
||||
element += "}";
|
||||
haveElement = true;
|
||||
}
|
||||
if (haveElement) {
|
||||
if (!first)
|
||||
out += ",";
|
||||
out += element;
|
||||
first = false;
|
||||
}
|
||||
file.close();
|
||||
file = root.openNextFile();
|
||||
}
|
||||
root.close();
|
||||
return fileList;
|
||||
out += "]";
|
||||
return out;
|
||||
}
|
||||
|
||||
void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res)
|
||||
@@ -318,28 +389,25 @@ void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res)
|
||||
res->setHeader("Access-Control-Allow-Methods", "GET");
|
||||
|
||||
concurrency::LockGuard g(spiLock);
|
||||
auto fileList = htmlListDir("/static", 10);
|
||||
std::string fileList = htmlListDir("/static", 10);
|
||||
|
||||
// create json output structure
|
||||
JSONObject filesystemObj;
|
||||
filesystemObj["total"] = new JSONValue((int)FSCom.totalBytes());
|
||||
filesystemObj["used"] = new JSONValue((int)FSCom.usedBytes());
|
||||
filesystemObj["free"] = new JSONValue(int(FSCom.totalBytes() - FSCom.usedBytes()));
|
||||
uint64_t total = FSCom.totalBytes();
|
||||
uint64_t used = FSCom.usedBytes();
|
||||
|
||||
JSONObject jsonObjInner;
|
||||
jsonObjInner["files"] = new JSONValue(fileList);
|
||||
jsonObjInner["filesystem"] = new JSONValue(filesystemObj);
|
||||
// Key order matches the previous std::map-based emission (alphabetical).
|
||||
std::string out;
|
||||
out.reserve(fileList.size() + 128);
|
||||
out += "{\"data\":{\"files\":";
|
||||
out += fileList;
|
||||
out += ",\"filesystem\":{\"free\":";
|
||||
out += jsonNum((int)(total - used));
|
||||
out += ",\"total\":";
|
||||
out += jsonNum((int)total);
|
||||
out += ",\"used\":";
|
||||
out += jsonNum((int)used);
|
||||
out += "}},\"status\":\"ok\"}";
|
||||
|
||||
JSONObject jsonObjOuter;
|
||||
jsonObjOuter["data"] = new JSONValue(jsonObjInner);
|
||||
jsonObjOuter["status"] = new JSONValue("ok");
|
||||
|
||||
JSONValue *value = new JSONValue(jsonObjOuter);
|
||||
|
||||
std::string jsonString = value->Stringify();
|
||||
res->print(jsonString.c_str());
|
||||
|
||||
delete value;
|
||||
res->print(out.c_str());
|
||||
}
|
||||
|
||||
void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res)
|
||||
@@ -354,27 +422,13 @@ void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res)
|
||||
if (params->getQueryParameter("delete", paramValDelete)) {
|
||||
std::string pathDelete = "/" + paramValDelete;
|
||||
concurrency::LockGuard g(spiLock);
|
||||
if (FSCom.remove(pathDelete.c_str())) {
|
||||
|
||||
LOG_INFO("%s", pathDelete.c_str());
|
||||
JSONObject jsonObjOuter;
|
||||
jsonObjOuter["status"] = new JSONValue("ok");
|
||||
JSONValue *value = new JSONValue(jsonObjOuter);
|
||||
std::string jsonString = value->Stringify();
|
||||
res->print(jsonString.c_str());
|
||||
delete value;
|
||||
return;
|
||||
} else {
|
||||
|
||||
LOG_INFO("%s", pathDelete.c_str());
|
||||
JSONObject jsonObjOuter;
|
||||
jsonObjOuter["status"] = new JSONValue("Error");
|
||||
JSONValue *value = new JSONValue(jsonObjOuter);
|
||||
std::string jsonString = value->Stringify();
|
||||
res->print(jsonString.c_str());
|
||||
delete value;
|
||||
return;
|
||||
}
|
||||
const char *status = FSCom.remove(pathDelete.c_str()) ? "ok" : "Error";
|
||||
LOG_INFO("%s", pathDelete.c_str());
|
||||
std::string out = "{\"status\":";
|
||||
out += jsonEscape(status);
|
||||
out += "}";
|
||||
res->print(out.c_str());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -615,95 +669,113 @@ void handleReport(HTTPRequest *req, HTTPResponse *res)
|
||||
res->println("<pre>");
|
||||
}
|
||||
|
||||
// Helper lambda to create JSON array and clean up memory properly
|
||||
auto createJSONArrayFromLog = [](const uint32_t *logArray, int count) -> JSONValue * {
|
||||
JSONArray tempArray;
|
||||
auto arrayFromLog = [](const uint32_t *logArray, int count) -> std::string {
|
||||
std::string s = "[";
|
||||
for (int i = 0; i < count; i++) {
|
||||
tempArray.push_back(new JSONValue((int)logArray[i]));
|
||||
if (i)
|
||||
s += ",";
|
||||
s += jsonNum((int)logArray[i]);
|
||||
}
|
||||
JSONValue *result = new JSONValue(tempArray);
|
||||
// Note: Don't delete tempArray elements here - JSONValue now owns them
|
||||
return result;
|
||||
s += "]";
|
||||
return s;
|
||||
};
|
||||
|
||||
// data->airtime->tx_log
|
||||
uint32_t *logArray;
|
||||
logArray = airTime->airtimeReport(TX_LOG);
|
||||
JSONValue *txLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
|
||||
|
||||
// data->airtime->rx_log
|
||||
std::string txLog = arrayFromLog(logArray, airTime->getPeriodsToLog());
|
||||
logArray = airTime->airtimeReport(RX_LOG);
|
||||
JSONValue *rxLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
|
||||
|
||||
// data->airtime->rx_all_log
|
||||
std::string rxLog = arrayFromLog(logArray, airTime->getPeriodsToLog());
|
||||
logArray = airTime->airtimeReport(RX_ALL_LOG);
|
||||
JSONValue *rxAllLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
|
||||
std::string rxAllLog = arrayFromLog(logArray, airTime->getPeriodsToLog());
|
||||
|
||||
// data->airtime
|
||||
JSONObject jsonObjAirtime;
|
||||
jsonObjAirtime["tx_log"] = txLogJsonValue;
|
||||
jsonObjAirtime["rx_log"] = rxLogJsonValue;
|
||||
jsonObjAirtime["rx_all_log"] = rxAllLogJsonValue;
|
||||
jsonObjAirtime["channel_utilization"] = new JSONValue(airTime->channelUtilizationPercent());
|
||||
jsonObjAirtime["utilization_tx"] = new JSONValue(airTime->utilizationTXPercent());
|
||||
jsonObjAirtime["seconds_since_boot"] = new JSONValue(int(airTime->getSecondsSinceBoot()));
|
||||
jsonObjAirtime["seconds_per_period"] = new JSONValue(int(airTime->getSecondsPerPeriod()));
|
||||
jsonObjAirtime["periods_to_log"] = new JSONValue(airTime->getPeriodsToLog());
|
||||
|
||||
// data->wifi
|
||||
JSONObject jsonObjWifi;
|
||||
jsonObjWifi["rssi"] = new JSONValue(WiFi.RSSI());
|
||||
String wifiIPString = WiFi.localIP().toString();
|
||||
std::string wifiIP = wifiIPString.c_str();
|
||||
jsonObjWifi["ip"] = new JSONValue(wifiIP.c_str());
|
||||
|
||||
// data->memory
|
||||
JSONObject jsonObjMemory;
|
||||
jsonObjMemory["heap_total"] = new JSONValue((int)memGet.getHeapSize());
|
||||
jsonObjMemory["heap_free"] = new JSONValue((int)memGet.getFreeHeap());
|
||||
jsonObjMemory["psram_total"] = new JSONValue((int)memGet.getPsramSize());
|
||||
jsonObjMemory["psram_free"] = new JSONValue((int)memGet.getFreePsram());
|
||||
spiLock->lock();
|
||||
jsonObjMemory["fs_total"] = new JSONValue((int)FSCom.totalBytes());
|
||||
jsonObjMemory["fs_used"] = new JSONValue((int)FSCom.usedBytes());
|
||||
jsonObjMemory["fs_free"] = new JSONValue(int(FSCom.totalBytes() - FSCom.usedBytes()));
|
||||
uint64_t fsTotal = FSCom.totalBytes();
|
||||
uint64_t fsUsed = FSCom.usedBytes();
|
||||
spiLock->unlock();
|
||||
|
||||
// data->power
|
||||
JSONObject jsonObjPower;
|
||||
jsonObjPower["battery_percent"] = new JSONValue(powerStatus->getBatteryChargePercent());
|
||||
jsonObjPower["battery_voltage_mv"] = new JSONValue(powerStatus->getBatteryVoltageMv());
|
||||
jsonObjPower["has_battery"] = new JSONValue(BoolToString(powerStatus->getHasBattery()));
|
||||
jsonObjPower["has_usb"] = new JSONValue(BoolToString(powerStatus->getHasUSB()));
|
||||
jsonObjPower["is_charging"] = new JSONValue(BoolToString(powerStatus->getIsCharging()));
|
||||
// Emit keys in the same alphabetical order as the previous
|
||||
// std::map-based JSON output to keep responses byte-compatible.
|
||||
std::string out;
|
||||
out.reserve(1024);
|
||||
out += "{\"data\":{";
|
||||
|
||||
// data->device
|
||||
JSONObject jsonObjDevice;
|
||||
jsonObjDevice["reboot_counter"] = new JSONValue((int)myNodeInfo.reboot_count);
|
||||
// airtime
|
||||
out += "\"airtime\":{";
|
||||
out += "\"channel_utilization\":";
|
||||
out += jsonNum(airTime->channelUtilizationPercent());
|
||||
out += ",\"periods_to_log\":";
|
||||
out += jsonNum(airTime->getPeriodsToLog());
|
||||
out += ",\"rx_all_log\":";
|
||||
out += rxAllLog;
|
||||
out += ",\"rx_log\":";
|
||||
out += rxLog;
|
||||
out += ",\"seconds_per_period\":";
|
||||
out += jsonNum((int)airTime->getSecondsPerPeriod());
|
||||
out += ",\"seconds_since_boot\":";
|
||||
out += jsonNum((int)airTime->getSecondsSinceBoot());
|
||||
out += ",\"tx_log\":";
|
||||
out += txLog;
|
||||
out += ",\"utilization_tx\":";
|
||||
out += jsonNum(airTime->utilizationTXPercent());
|
||||
out += "}";
|
||||
|
||||
// data->radio
|
||||
JSONObject jsonObjRadio;
|
||||
jsonObjRadio["frequency"] = new JSONValue(RadioLibInterface::instance->getFreq());
|
||||
jsonObjRadio["lora_channel"] = new JSONValue((int)RadioLibInterface::instance->getChannelNum() + 1);
|
||||
// device
|
||||
out += ",\"device\":{\"reboot_counter\":";
|
||||
out += jsonNum((int)myNodeInfo.reboot_count);
|
||||
out += "}";
|
||||
|
||||
// collect data to inner data object
|
||||
JSONObject jsonObjInner;
|
||||
jsonObjInner["airtime"] = new JSONValue(jsonObjAirtime);
|
||||
jsonObjInner["wifi"] = new JSONValue(jsonObjWifi);
|
||||
jsonObjInner["memory"] = new JSONValue(jsonObjMemory);
|
||||
jsonObjInner["power"] = new JSONValue(jsonObjPower);
|
||||
jsonObjInner["device"] = new JSONValue(jsonObjDevice);
|
||||
jsonObjInner["radio"] = new JSONValue(jsonObjRadio);
|
||||
// memory
|
||||
out += ",\"memory\":{";
|
||||
out += "\"fs_free\":";
|
||||
out += jsonNum((int)(fsTotal - fsUsed));
|
||||
out += ",\"fs_total\":";
|
||||
out += jsonNum((int)fsTotal);
|
||||
out += ",\"fs_used\":";
|
||||
out += jsonNum((int)fsUsed);
|
||||
out += ",\"heap_free\":";
|
||||
out += jsonNum((int)memGet.getFreeHeap());
|
||||
out += ",\"heap_total\":";
|
||||
out += jsonNum((int)memGet.getHeapSize());
|
||||
out += ",\"psram_free\":";
|
||||
out += jsonNum((int)memGet.getFreePsram());
|
||||
out += ",\"psram_total\":";
|
||||
out += jsonNum((int)memGet.getPsramSize());
|
||||
out += "}";
|
||||
|
||||
// create json output structure
|
||||
JSONObject jsonObjOuter;
|
||||
jsonObjOuter["data"] = new JSONValue(jsonObjInner);
|
||||
jsonObjOuter["status"] = new JSONValue("ok");
|
||||
// serialize and write it to the stream
|
||||
JSONValue *value = new JSONValue(jsonObjOuter);
|
||||
std::string jsonString = value->Stringify();
|
||||
res->print(jsonString.c_str());
|
||||
delete value;
|
||||
// power (has_* / is_charging were serialized as the strings "true"/"false")
|
||||
out += ",\"power\":{";
|
||||
out += "\"battery_percent\":";
|
||||
out += jsonNum(powerStatus->getBatteryChargePercent());
|
||||
out += ",\"battery_voltage_mv\":";
|
||||
out += jsonNum(powerStatus->getBatteryVoltageMv());
|
||||
out += ",\"has_battery\":";
|
||||
out += jsonEscape(BoolToString(powerStatus->getHasBattery()));
|
||||
out += ",\"has_usb\":";
|
||||
out += jsonEscape(BoolToString(powerStatus->getHasUSB()));
|
||||
out += ",\"is_charging\":";
|
||||
out += jsonEscape(BoolToString(powerStatus->getIsCharging()));
|
||||
out += "}";
|
||||
|
||||
// radio
|
||||
out += ",\"radio\":{\"frequency\":";
|
||||
out += jsonNum(RadioLibInterface::instance->getFreq());
|
||||
out += ",\"lora_channel\":";
|
||||
out += jsonNum((int)RadioLibInterface::instance->getChannelNum() + 1);
|
||||
out += "}";
|
||||
|
||||
// wifi
|
||||
out += ",\"wifi\":{\"ip\":";
|
||||
out += jsonEscape(wifiIP);
|
||||
out += ",\"rssi\":";
|
||||
out += jsonNum(WiFi.RSSI());
|
||||
out += "}";
|
||||
|
||||
out += "},\"status\":\"ok\"}";
|
||||
|
||||
res->print(out.c_str());
|
||||
}
|
||||
|
||||
void handleNodes(HTTPRequest *req, HTTPResponse *res)
|
||||
@@ -724,58 +796,65 @@ void handleNodes(HTTPRequest *req, HTTPResponse *res)
|
||||
res->println("<pre>");
|
||||
}
|
||||
|
||||
JSONArray nodesArray;
|
||||
std::string out;
|
||||
out.reserve(2048);
|
||||
out += "{\"data\":{\"nodes\":[";
|
||||
|
||||
bool firstNode = true;
|
||||
uint32_t readIndex = 0;
|
||||
const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
|
||||
while (tempNodeInfo != NULL) {
|
||||
if (tempNodeInfo->has_user) {
|
||||
JSONObject node;
|
||||
|
||||
char id[16];
|
||||
snprintf(id, sizeof(id), "!%08x", tempNodeInfo->num);
|
||||
|
||||
node["id"] = new JSONValue(id);
|
||||
node["snr"] = new JSONValue(tempNodeInfo->snr);
|
||||
node["via_mqtt"] = new JSONValue(BoolToString(tempNodeInfo->via_mqtt));
|
||||
node["last_heard"] = new JSONValue((int)tempNodeInfo->last_heard);
|
||||
node["position"] = new JSONValue();
|
||||
|
||||
if (nodeDB->hasValidPosition(tempNodeInfo)) {
|
||||
JSONObject position;
|
||||
position["latitude"] = new JSONValue((float)tempNodeInfo->position.latitude_i * 1e-7);
|
||||
position["longitude"] = new JSONValue((float)tempNodeInfo->position.longitude_i * 1e-7);
|
||||
position["altitude"] = new JSONValue((int)tempNodeInfo->position.altitude);
|
||||
node["position"] = new JSONValue(position);
|
||||
}
|
||||
|
||||
node["long_name"] = new JSONValue(tempNodeInfo->user.long_name);
|
||||
node["short_name"] = new JSONValue(tempNodeInfo->user.short_name);
|
||||
char macStr[18];
|
||||
snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X", tempNodeInfo->user.macaddr[0],
|
||||
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));
|
||||
std::string position;
|
||||
if (nodeDB->hasValidPosition(tempNodeInfo)) {
|
||||
position = "{\"altitude\":";
|
||||
position += jsonNum((int)tempNodeInfo->position.altitude);
|
||||
position += ",\"latitude\":";
|
||||
position += jsonNum((float)tempNodeInfo->position.latitude_i * 1e-7);
|
||||
position += ",\"longitude\":";
|
||||
position += jsonNum((float)tempNodeInfo->position.longitude_i * 1e-7);
|
||||
position += "}";
|
||||
} else {
|
||||
position = "null";
|
||||
}
|
||||
|
||||
if (!firstNode)
|
||||
out += ",";
|
||||
firstNode = false;
|
||||
|
||||
// Alphabetical key order matches previous std::map-based output.
|
||||
out += "{\"hw_model\":";
|
||||
out += jsonNum(tempNodeInfo->user.hw_model);
|
||||
out += ",\"id\":";
|
||||
out += jsonEscape(id);
|
||||
out += ",\"last_heard\":";
|
||||
out += jsonNum((int)tempNodeInfo->last_heard);
|
||||
out += ",\"long_name\":";
|
||||
out += jsonEscape(tempNodeInfo->user.long_name);
|
||||
out += ",\"mac_address\":";
|
||||
out += jsonEscape(macStr);
|
||||
out += ",\"position\":";
|
||||
out += position;
|
||||
out += ",\"short_name\":";
|
||||
out += jsonEscape(tempNodeInfo->user.short_name);
|
||||
out += ",\"snr\":";
|
||||
out += jsonNum(tempNodeInfo->snr);
|
||||
out += ",\"via_mqtt\":";
|
||||
out += jsonEscape(BoolToString(tempNodeInfo->via_mqtt));
|
||||
out += "}";
|
||||
}
|
||||
tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
|
||||
}
|
||||
|
||||
// collect data to inner data object
|
||||
JSONObject jsonObjInner;
|
||||
jsonObjInner["nodes"] = new JSONValue(nodesArray);
|
||||
|
||||
// create json output structure
|
||||
JSONObject jsonObjOuter;
|
||||
jsonObjOuter["data"] = new JSONValue(jsonObjInner);
|
||||
jsonObjOuter["status"] = new JSONValue("ok");
|
||||
// serialize and write it to the stream
|
||||
JSONValue *value = new JSONValue(jsonObjOuter);
|
||||
std::string jsonString = value->Stringify();
|
||||
res->print(jsonString.c_str());
|
||||
delete value;
|
||||
out += "]},\"status\":\"ok\"}";
|
||||
res->print(out.c_str());
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -897,20 +976,28 @@ void handleScanNetworks(HTTPRequest *req, HTTPResponse *res)
|
||||
|
||||
int n = WiFi.scanNetworks();
|
||||
|
||||
// build list of network objects
|
||||
JSONArray networkObjs;
|
||||
std::string out = "{\"data\":[";
|
||||
bool firstNet = true;
|
||||
if (n > 0) {
|
||||
for (int i = 0; i < n; ++i) {
|
||||
char ssidArray[50];
|
||||
// The previous implementation pre-escaped quotes before handing
|
||||
// the value to the JSON serializer; preserve that (byte-compatible
|
||||
// even if it double-encodes a quote) so existing clients are not
|
||||
// affected by this refactor.
|
||||
String ssidString = String(WiFi.SSID(i));
|
||||
ssidString.replace("\"", "\\\"");
|
||||
ssidString.toCharArray(ssidArray, 50);
|
||||
|
||||
if (WiFi.encryptionType(i) != WIFI_AUTH_OPEN) {
|
||||
JSONObject thisNetwork;
|
||||
thisNetwork["ssid"] = new JSONValue(ssidArray);
|
||||
thisNetwork["rssi"] = new JSONValue(int(WiFi.RSSI(i)));
|
||||
networkObjs.push_back(new JSONValue(thisNetwork));
|
||||
if (!firstNet)
|
||||
out += ",";
|
||||
firstNet = false;
|
||||
out += "{\"rssi\":";
|
||||
out += jsonNum((int)WiFi.RSSI(i));
|
||||
out += ",\"ssid\":";
|
||||
out += jsonEscape(ssidArray);
|
||||
out += "}";
|
||||
}
|
||||
// Yield some cpu cycles to IP stack.
|
||||
// This is important in case the list is large and it takes us time to return
|
||||
@@ -918,16 +1005,7 @@ void handleScanNetworks(HTTPRequest *req, HTTPResponse *res)
|
||||
yield();
|
||||
}
|
||||
}
|
||||
|
||||
// build output structure
|
||||
JSONObject jsonObjOuter;
|
||||
jsonObjOuter["data"] = new JSONValue(networkObjs);
|
||||
jsonObjOuter["status"] = new JSONValue("ok");
|
||||
|
||||
// serialize and write it to the stream
|
||||
JSONValue *value = new JSONValue(jsonObjOuter);
|
||||
std::string jsonString = value->Stringify();
|
||||
res->print(jsonString.c_str());
|
||||
delete value;
|
||||
out += "],\"status\":\"ok\"}";
|
||||
res->print(out.c_str());
|
||||
}
|
||||
#endif
|
||||
@@ -480,7 +480,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
||||
#if HAS_SCREEN
|
||||
IF_SCREEN(screen->showSimpleBanner("Device is rebooting\ninto DFU mode.", 0));
|
||||
#endif
|
||||
#if defined(ARCH_NRF52) || defined(ARCH_RP2040)
|
||||
#if defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)
|
||||
enterDfuMode();
|
||||
#endif
|
||||
break;
|
||||
|
||||
@@ -104,10 +104,6 @@
|
||||
#include "modules/StatusMessageModule.h"
|
||||
#endif
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ZPS
|
||||
#include "modules/esp32/ZPSModule.h"
|
||||
#endif
|
||||
|
||||
#if defined(HAS_HARDWARE_WATCHDOG)
|
||||
#include "watchdog/watchdogThread.h"
|
||||
#endif
|
||||
@@ -178,9 +174,6 @@ void setupModules()
|
||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||
statusMessageModule = new StatusMessageModule();
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_ZPS
|
||||
zpsModule = new ZPSModule();
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE
|
||||
new GenericThreadModule();
|
||||
#endif
|
||||
|
||||
@@ -29,10 +29,23 @@ int32_t StatusMessageModule::runOnce()
|
||||
ProcessMessage StatusMessageModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
|
||||
meshtastic_StatusMessage incomingMessage;
|
||||
meshtastic_StatusMessage incomingMessage = meshtastic_StatusMessage_init_zero;
|
||||
|
||||
if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_StatusMessage_fields,
|
||||
&incomingMessage)) {
|
||||
|
||||
LOG_INFO("Received a NodeStatus message %s", incomingMessage.status);
|
||||
|
||||
RecentStatus entry;
|
||||
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;
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||
#include "SinglePortModule.h"
|
||||
#include "configuration.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class StatusMessageModule : public SinglePortModule, private concurrency::OSThread
|
||||
{
|
||||
|
||||
public:
|
||||
/** Constructor
|
||||
* name is for debugging output
|
||||
@@ -19,16 +20,28 @@ class StatusMessageModule : public SinglePortModule, private concurrency::OSThre
|
||||
this->setInterval(1000 * 12 * 60 * 60);
|
||||
}
|
||||
// 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;
|
||||
|
||||
struct RecentStatus {
|
||||
uint32_t fromNodeId; // mp.from
|
||||
std::string statusText; // incomingMessage.status
|
||||
};
|
||||
|
||||
const std::vector<RecentStatus> &getRecentReceived() const { return recentReceived; }
|
||||
|
||||
protected:
|
||||
/** Called to handle a particular incoming message
|
||||
*/
|
||||
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
|
||||
private:
|
||||
static constexpr size_t MAX_RECENT_STATUSMESSAGES = 5;
|
||||
std::vector<RecentStatus> recentReceived;
|
||||
};
|
||||
|
||||
extern StatusMessageModule *statusMessageModule;
|
||||
|
||||
@@ -1,419 +0,0 @@
|
||||
/*
|
||||
* ZPS - Zero-GPS Positioning System for standalone Meshtastic devices
|
||||
* - experimental tools for estimating own position without a GPS -
|
||||
*
|
||||
* Copyright 2021 all rights reserved by https://github.com/a-f-G-U-C
|
||||
* Released under GPL v3 (see LICENSE file for details)
|
||||
*/
|
||||
|
||||
#include "ZPSModule.h"
|
||||
#include "Default.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "NodeStatus.h"
|
||||
#include "Router.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/RTC.h"
|
||||
#include <WiFi.h>
|
||||
|
||||
#if !defined(MESHTASTIC_EXCLUDE_BLUETOOTH)
|
||||
|
||||
#include "NimBLEDevice.h"
|
||||
|
||||
#define BLE_MAX_REC 15
|
||||
#define BLE_NO_RESULTS -1 // Indicates a BLE scan is in progress
|
||||
|
||||
uint8_t bleCounter = 0; // used internally by the ble scanner
|
||||
uint64_t bleResult[BLE_MAX_REC + 1];
|
||||
int bleResSize = BLE_NO_RESULTS;
|
||||
|
||||
uint64_t scanStart = 0;
|
||||
|
||||
ZPSModule *zpsModule;
|
||||
|
||||
// Mini BLE scanner, NIMBLE based and modelled loosely after the Wifi scanner
|
||||
static int ble_scan(uint32_t duration, bool passive = true, bool dedup = true);
|
||||
|
||||
// ZPSModule::ZPSModule()
|
||||
// : ProtobufModule("ZPS", ZPS_PORTNUM, Position_fields), concurrency::OSThread("ZPSModule")
|
||||
ZPSModule::ZPSModule() : SinglePortModule("ZPS", ZPS_PORTNUM), concurrency::OSThread("ZPSModule")
|
||||
{
|
||||
setIntervalFromNow(ZPS_STARTUP_DELAY); // Delay startup by 10 seconds, no need to race :)
|
||||
|
||||
wantBSS = true;
|
||||
wantBLE = true;
|
||||
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.disconnect();
|
||||
WiFi.scanNetworks(true, true); // nonblock, showhidden
|
||||
scanState = SCAN_BSS_RUN;
|
||||
}
|
||||
|
||||
ProcessMessage ZPSModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
meshtastic_Position pos = meshtastic_Position_init_default;
|
||||
|
||||
auto &pd = mp.decoded;
|
||||
uint8_t nRecs = pd.payload.size >> 3;
|
||||
|
||||
LOG_DEBUG("handleReceived %s 0x%0x->0x%0x, id=0x%x, port=%d, len=%d, rec=%d\n", name, mp.from, mp.to, mp.id, pd.portnum,
|
||||
pd.payload.size, nRecs);
|
||||
if (nRecs > ZPS_DATAPKT_MAXITEMS)
|
||||
nRecs = ZPS_DATAPKT_MAXITEMS;
|
||||
memcpy(&netData, pd.payload.bytes, nRecs << 3);
|
||||
|
||||
// Currently we are unable to act as a position server, so we're
|
||||
// not interested in broadcasts (this will change later)
|
||||
if (mp.to != nodeDB->getNodeNum()) {
|
||||
// Message is not for us, won't process
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
#ifdef ZPS_EXTRAVERBOSE
|
||||
for (int i = 0; i < nRecs; i++) {
|
||||
LOG_DEBUG("ZPS[%d]: %08x"
|
||||
"%08x\n",
|
||||
i, (uint32_t)(netData[i] >> 32), (uint32_t)netData[i]);
|
||||
}
|
||||
#endif
|
||||
|
||||
if ((netData[0] & 0x800000000000) && (nRecs >= 2)) {
|
||||
// message contains a position
|
||||
pos.PDOP = (netData[0] >> 40) & 0x7f;
|
||||
pos.timestamp = netData[0] & 0xffffffff;
|
||||
// second int64 encodes lat and lon
|
||||
pos.longitude_i = (int32_t)(netData[1] & 0xffffffff);
|
||||
pos.latitude_i = (int32_t)((netData[1] >> 32) & 0xffffffff);
|
||||
|
||||
// FIXME should be conditional, to ensure we don't overwrite a good GPS fix!
|
||||
LOG_DEBUG("ZPS lat/lon/dop/pts %d/%d/%d/%d\n", pos.latitude_i, pos.longitude_i, pos.PDOP, pos.timestamp);
|
||||
|
||||
// Some required fields
|
||||
pos.time = getTime();
|
||||
pos.location_source = meshtastic_Position_LocSource_LOC_EXTERNAL;
|
||||
|
||||
// don't update position if my gps fix is valid
|
||||
if (nodeDB->hasValidPosition(nodeDB->getMeshNode(nodeDB->getNodeNum()))) {
|
||||
LOG_DEBUG("ZPSModule::handleReceived: ignoring position update, GPS is valid\n");
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
nodeDB->updatePosition(nodeDB->getNodeNum(), pos);
|
||||
} else {
|
||||
// nothing we can do - for now
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
return ProcessMessage::CONTINUE; // Let others look at this message also if they want
|
||||
}
|
||||
|
||||
meshtastic_MeshPacket *ZPSModule::allocReply()
|
||||
{
|
||||
meshtastic_MeshPacket *p = allocDataPacket();
|
||||
p->decoded.payload.size = (netRecs + 2) << 3; // actually can be only +1 if no GPS data
|
||||
|
||||
LOG_DEBUG("Allocating dataPacket for %d items, %d bytes\n", netRecs, p->decoded.payload.size);
|
||||
memcpy(p->decoded.payload.bytes, &netData, p->decoded.payload.size);
|
||||
|
||||
return (p);
|
||||
}
|
||||
|
||||
void ZPSModule::sendDataPacket(NodeNum dest, bool wantReplies)
|
||||
{
|
||||
// cancel any not yet sent (now stale) position packets
|
||||
if (prevPacketId)
|
||||
service->cancelSending(prevPacketId);
|
||||
|
||||
meshtastic_MeshPacket *p = allocReply();
|
||||
p->to = dest;
|
||||
p->decoded.portnum = meshtastic_PortNum_ZPS_APP;
|
||||
p->decoded.want_response = wantReplies;
|
||||
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
prevPacketId = p->id;
|
||||
|
||||
service->sendToMesh(p, RX_SRC_LOCAL);
|
||||
}
|
||||
|
||||
int32_t ZPSModule::runOnce()
|
||||
{
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
assert(node);
|
||||
|
||||
// LOG_DEBUG("ZPSModule::runOnce() START, scanState: %d\n", (int) scanState);
|
||||
|
||||
int numWifi = 0;
|
||||
|
||||
if (scanState == SCAN_BSS_RUN) {
|
||||
// check completion status of any running Wifi scan
|
||||
numWifi = WiFi.scanComplete();
|
||||
|
||||
if (numWifi >= 0) {
|
||||
// scan is complete
|
||||
LOG_DEBUG("%d BSS found\n", numWifi);
|
||||
LOG_DEBUG("BSS scan done in %d millis\n", millis() - scanStart);
|
||||
|
||||
if (wantBSS && haveBSS) {
|
||||
// old data exists, overwrite it
|
||||
netRecs = 0;
|
||||
haveBSS = haveBLE = false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < numWifi; i++) {
|
||||
// pack each Wifi network record into a 64-bit int
|
||||
uint64_t netBytes = encodeBSS(WiFi.BSSID(i), WiFi.channel(i), abs(WiFi.RSSI(i)));
|
||||
|
||||
if (wantBSS) {
|
||||
// load into outbound array if needed
|
||||
outBufAdd(netBytes);
|
||||
haveBSS = true;
|
||||
}
|
||||
#ifdef ZPS_EXTRAVERBOSE
|
||||
LOG_DEBUG("BSS[%02d]: %08x"
|
||||
"%08x\n",
|
||||
i, (uint32_t)(netBytes >> 32), (uint32_t)netBytes);
|
||||
#endif
|
||||
}
|
||||
|
||||
WiFi.scanDelete();
|
||||
scanState = SCAN_BSS_DONE;
|
||||
|
||||
#ifdef ZPS_EXTRAVERBOSE
|
||||
} else if (numWifi == -1) {
|
||||
// LOG_DEBUG("BSS scan in-progress\n");
|
||||
} else {
|
||||
LOG_DEBUG("BSS scan state=%d\n", numWifi);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
if ((scanState == SCAN_BLE_RUN) && (bleResSize >= 0)) {
|
||||
// completion status checked above (bleResSize >= 0)
|
||||
LOG_DEBUG("BLE scan done in %d millis\n", millis() - scanStart);
|
||||
scanState = SCAN_BLE_DONE;
|
||||
|
||||
if (wantBLE && haveBLE) {
|
||||
// old data exists, overwrite it
|
||||
netRecs = 0;
|
||||
haveBSS = haveBLE = false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < bleResSize; i++) {
|
||||
// load data into output array if needed
|
||||
if (wantBLE) {
|
||||
outBufAdd(bleResult[i]);
|
||||
haveBLE = true;
|
||||
}
|
||||
#ifdef ZPS_EXTRAVERBOSE
|
||||
LOG_DEBUG("BLE[%d]: %08x"
|
||||
"%08x\n",
|
||||
i, (uint32_t)(bleResult[i] >> 32), (uint32_t)bleResult[i]);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Reset the counter once we're done with the dataset
|
||||
bleResSize = BLE_NO_RESULTS;
|
||||
}
|
||||
|
||||
// Are we finished assembling that packet? Then send it out
|
||||
if ((wantBSS == haveBSS) && (wantBLE == haveBLE) &&
|
||||
airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
|
||||
airTime->isTxAllowedAirUtil() &&
|
||||
(lastSend == 0 || millis() - lastSend >= Default::getConfiguredOrDefaultMsScaled(config.position.position_broadcast_secs,
|
||||
default_broadcast_interval_secs,
|
||||
nodeStatus->getNumOnline()))) {
|
||||
|
||||
haveBSS = haveBLE = false;
|
||||
sendDataPacket(NODENUM_BROADCAST, false); // no replies
|
||||
lastSend = millis();
|
||||
netRecs = 0; // reset packet
|
||||
}
|
||||
|
||||
/*
|
||||
* State machine transitions
|
||||
*
|
||||
* FIXME could be managed better, for example: check if we require
|
||||
* each type of scan (wantBSS/wantBLE), and if not, don't start it!
|
||||
*/
|
||||
if (scanState == SCAN_BLE_DONE) {
|
||||
// BLE done, transition to BSS scanning
|
||||
scanStart = millis();
|
||||
LOG_DEBUG("BSS scan start t=%d\n", scanStart);
|
||||
if (WiFi.scanNetworks(true, true) == WIFI_SCAN_RUNNING) // nonblock, showhidden
|
||||
scanState = SCAN_BSS_RUN;
|
||||
|
||||
} else if (scanState == SCAN_BSS_DONE) {
|
||||
// BSS done, transition to BLE scanning
|
||||
scanStart = millis();
|
||||
LOG_DEBUG("BLE scan start t=%d\n", scanStart);
|
||||
if (ble_scan(ZPS_BLE_SCANTIME) == 0)
|
||||
scanState = SCAN_BLE_RUN;
|
||||
}
|
||||
|
||||
// LOG_DEBUG("ZPSModule::runOnce() DONE, scanState=%d\n", scanState);
|
||||
if ((scanState == SCAN_BSS_RUN) || (scanState == SCAN_BLE_RUN)) {
|
||||
return 1000; // scan in progress, re-check soon
|
||||
}
|
||||
|
||||
return 5000;
|
||||
}
|
||||
|
||||
uint64_t encodeBSS(uint8_t *bssid, uint8_t chan, uint8_t absRSSI)
|
||||
{
|
||||
uint64_t netBytes = absRSSI & 0xff;
|
||||
netBytes <<= 8;
|
||||
netBytes |= (chan & 0xff);
|
||||
|
||||
for (uint8_t b = 0; b < 6; b++) {
|
||||
netBytes <<= 8;
|
||||
netBytes |= bssid[b];
|
||||
}
|
||||
|
||||
return netBytes;
|
||||
}
|
||||
|
||||
uint64_t encodeBLE(uint8_t *addr, uint8_t absRSSI)
|
||||
{
|
||||
uint64_t netBytes = absRSSI & 0xff;
|
||||
netBytes <<= 8;
|
||||
netBytes |= 0xff; // "channel" byte reserved in BLE records
|
||||
|
||||
for (uint8_t b = 0; b < 6; b++) {
|
||||
netBytes <<= 8;
|
||||
netBytes |= addr[5 - b] & 0xff;
|
||||
}
|
||||
|
||||
return netBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler
|
||||
*/
|
||||
static int ble_gap_event(struct ble_gap_event *event, void *arg)
|
||||
{
|
||||
// Adverts matching certain patterns are useless for positioning purposes
|
||||
// (ephemeral MAC etc), so try excluding them if possible
|
||||
//
|
||||
// TODO: Expand the list of reject patterns for BLE adverts.
|
||||
// There are likely more than 10 patterns to test and reject, including most Apple devices and others.
|
||||
//
|
||||
// TODO: Implement full packet search for reject patterns (use memmem() or similar),
|
||||
// not just at the beginning (currently uses memcmp()).
|
||||
|
||||
const uint8_t rejPat[] = {0x1e, 0xff, 0x06, 0x00, 0x01}; // one of many
|
||||
|
||||
struct ble_hs_adv_fields fields;
|
||||
int rc;
|
||||
int i = 0;
|
||||
|
||||
uint64_t netBytes = 0;
|
||||
|
||||
switch (event->type) {
|
||||
case BLE_GAP_EVENT_DISC:
|
||||
// called once for every BLE advert received
|
||||
rc = ble_hs_adv_parse_fields(&fields, event->disc.data, event->disc.length_data);
|
||||
if (rc != 0)
|
||||
return 0;
|
||||
|
||||
if (bleResSize != BLE_NO_RESULTS)
|
||||
// as far as we know, we're not in the middle of a BLE scan!
|
||||
LOG_DEBUG("Unexpected BLE_GAP_EVENT_DISC!\n");
|
||||
|
||||
#ifdef ZPS_EXTRAVERBOSE
|
||||
// Dump the advertisement packet
|
||||
DEBUG_PORT.hexDump("DEBUG", (unsigned char *)event->disc.data, event->disc.length_data);
|
||||
#endif
|
||||
// Reject beacons known to be unreliable (ephemeral etc)
|
||||
if (memcmp(event->disc.data, rejPat, sizeof(rejPat)) == 0) {
|
||||
LOG_DEBUG("(BLE item filtered by pattern)\n");
|
||||
return 0; // Processing-wise, it's still a success
|
||||
}
|
||||
|
||||
//
|
||||
// STORE THE RESULTS IN A SORTED LIST
|
||||
//
|
||||
|
||||
// first, pack each BLE item reading into a 64-bit int
|
||||
netBytes = encodeBLE(event->disc.addr.val, abs(event->disc.rssi));
|
||||
|
||||
// SOME DUPLICATES SURVIVE through filter_duplicates = 1, catch them here
|
||||
// Duplicate filtering is now handled in the sorting loop below,
|
||||
// but right now we write for clarity not optimization
|
||||
for (i = 0; i < bleCounter; i++) {
|
||||
if ((bleResult[i] & 0xffffffffffff) == (netBytes & 0xffffffffffff)) {
|
||||
LOG_DEBUG("(BLE duplicate filtered)\n");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ZPS_EXTRAVERBOSE
|
||||
// redundant extraverbosity, but I need it for duplicate hunting
|
||||
LOG_DEBUG("BL_[%02d]: %08x"
|
||||
"%08x\n",
|
||||
bleCounter, (uint32_t)(netBytes >> 32), (uint32_t)netBytes);
|
||||
#endif
|
||||
// then insert item into a list (up to BLE_MAX_REC records), sorted by RSSI
|
||||
for (i = 0; i < bleCounter; i++) {
|
||||
// find first element greater than ours, that will be our insertion point
|
||||
if (bleResult[i] > netBytes)
|
||||
break;
|
||||
}
|
||||
// any other records move down one position to vacate res[i]
|
||||
for (int j = bleCounter; j > i; j--)
|
||||
bleResult[j] = bleResult[j - 1];
|
||||
// write new element at insertion point
|
||||
bleResult[i] = netBytes;
|
||||
|
||||
// advance tail of list, but not beyond limit
|
||||
if (bleCounter < BLE_MAX_REC)
|
||||
bleCounter++;
|
||||
|
||||
return 0; // SUCCESS
|
||||
|
||||
case BLE_GAP_EVENT_DISC_COMPLETE:
|
||||
LOG_DEBUG("EVENT_DISC_COMPLETE in %d millis\n", (millis() - scanStart));
|
||||
LOG_DEBUG("%d BLE found\n", bleCounter);
|
||||
bleResSize = bleCounter;
|
||||
|
||||
bleCounter = 0; // reset counter
|
||||
return 0; // SUCCESS
|
||||
|
||||
default:
|
||||
return 0; // SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates the GAP general discovery procedure (non-blocking)
|
||||
*/
|
||||
static int ble_scan(uint32_t duration, bool passive, bool dedup)
|
||||
{
|
||||
uint8_t own_addr_type;
|
||||
struct ble_gap_disc_params disc_params;
|
||||
int rc;
|
||||
|
||||
// Figure out address type to use
|
||||
rc = ble_hs_id_infer_auto(0, &own_addr_type);
|
||||
if (rc != 0) {
|
||||
LOG_DEBUG("error determining address type; rc=%d\n", rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
// Scanning parameters, these are mostly default
|
||||
disc_params.itvl = 0;
|
||||
disc_params.window = 0;
|
||||
disc_params.filter_policy = 0;
|
||||
disc_params.limited = 0;
|
||||
|
||||
// These two params are the more interesting ones
|
||||
disc_params.filter_duplicates = dedup; // self-explanatory
|
||||
disc_params.passive = passive; // passive uses less power
|
||||
|
||||
// Start scanning process (non-blocking) and return
|
||||
rc = ble_gap_disc(own_addr_type, duration, &disc_params, ble_gap_event, NULL);
|
||||
if (rc != 0) {
|
||||
LOG_DEBUG("error initiating GAP discovery; rc=%d\n", rc);
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
#endif // MESHTASTIC_EXCLUDE_BLUETOOTH
|
||||
@@ -1,86 +0,0 @@
|
||||
#pragma once
|
||||
#include "SinglePortModule.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "gps/RTC.h"
|
||||
|
||||
#define ZPS_PORTNUM meshtastic_PortNum_ZPS_APP
|
||||
|
||||
#define ZPS_DATAPKT_MAXITEMS 20 // max number of records to pack in an outbound packet (~10)
|
||||
#define ZPS_STARTUP_DELAY 10000 // Module startup delay in millis
|
||||
|
||||
// Duration of a BLE scan in millis.
|
||||
// We want this number to be SLIGHTLY UNDER an integer number of seconds,
|
||||
// to be able to catch the result as fresh as possible on a 1-second polling loop
|
||||
#define ZPS_BLE_SCANTIME 2900 // millis
|
||||
|
||||
enum SCANSTATE { SCAN_NONE, SCAN_BSS_RUN, SCAN_BSS_DONE, SCAN_BLE_RUN, SCAN_BLE_DONE };
|
||||
|
||||
/*
|
||||
* Data packing "compression" functions
|
||||
* Ingest a WiFi BSSID, channel and RSSI (or BLE address and RSSI)
|
||||
* and encode them into a packed uint64
|
||||
*/
|
||||
uint64_t encodeBSS(uint8_t *bssid, uint8_t chan, uint8_t absRSSI);
|
||||
uint64_t encodeBLE(uint8_t *addr, uint8_t absRSSI);
|
||||
|
||||
class ZPSModule : public SinglePortModule, private concurrency::OSThread
|
||||
{
|
||||
/// The id of the last packet we sent, to allow us to cancel it if we make something fresher
|
||||
PacketId prevPacketId = 0;
|
||||
|
||||
/// We limit our broadcasts to a max rate
|
||||
uint32_t lastSend = 0;
|
||||
|
||||
bool wantBSS = true;
|
||||
bool haveBSS = false;
|
||||
|
||||
bool wantBLE = true;
|
||||
bool haveBLE = false;
|
||||
|
||||
public:
|
||||
/** Constructor
|
||||
* name is for debugging output
|
||||
*/
|
||||
ZPSModule();
|
||||
|
||||
/**
|
||||
* Send our radio environment data into the mesh
|
||||
*/
|
||||
void sendDataPacket(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false);
|
||||
|
||||
protected:
|
||||
/** Called to handle a particular incoming message
|
||||
@return true if you've guaranteed you've handled this message and no other handlers should be considered for it
|
||||
*/
|
||||
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp);
|
||||
|
||||
/** Messages can be received that have the want_response bit set. If set, this callback will be invoked
|
||||
* so that subclasses can (optionally) send a response back to the original sender. */
|
||||
virtual meshtastic_MeshPacket *allocReply();
|
||||
|
||||
/** Does our periodic broadcast */
|
||||
virtual int32_t runOnce();
|
||||
|
||||
private:
|
||||
// outbound data packet staging buffer and record counter
|
||||
uint64_t netData[ZPS_DATAPKT_MAXITEMS + 2] = {0};
|
||||
uint8_t netRecs = 0;
|
||||
|
||||
// mini state machine to alternate between BSS(Wifi) and BLE scanning
|
||||
SCANSTATE scanState = SCAN_NONE;
|
||||
|
||||
inline void outBufAdd(uint64_t netBytes)
|
||||
{
|
||||
// If this is the first record, initialize the header with the current time and reset the record count.
|
||||
if (!netRecs) {
|
||||
netData[0] = getTime();
|
||||
netData[1] = 0;
|
||||
}
|
||||
|
||||
// push to buffer and update counter
|
||||
if (netRecs < ZPS_DATAPKT_MAXITEMS)
|
||||
netData[2 + (netRecs++)] = netBytes;
|
||||
}
|
||||
};
|
||||
|
||||
extern ZPSModule *zpsModule;
|
||||
@@ -24,10 +24,6 @@
|
||||
#define ETH ETH2
|
||||
#endif // HAS_ETHERNET
|
||||
#include "Default.h"
|
||||
#if !defined(ARCH_NRF52) || NRF52_USE_JSON
|
||||
#include "serialization/JSON.h"
|
||||
#include "serialization/MeshPacketSerializer.h"
|
||||
#endif
|
||||
#include <Throttle.h>
|
||||
#include <assert.h>
|
||||
#include <utility>
|
||||
@@ -148,96 +144,6 @@ inline void onReceiveProto(char *topic, byte *payload, size_t length)
|
||||
router->enqueueReceivedMessage(p.release());
|
||||
}
|
||||
|
||||
#if !defined(ARCH_NRF52) || NRF52_USE_JSON
|
||||
// returns true if this is a valid JSON envelope which we accept on downlink
|
||||
inline bool isValidJsonEnvelope(JSONObject &json)
|
||||
{
|
||||
// Generate node ID from nodenum for comparison
|
||||
std::string nodeId = nodeDB->getNodeId();
|
||||
// if "sender" is provided, avoid processing packets we uplinked
|
||||
return (json.find("sender") != json.end() ? (json["sender"]->AsString().compare(nodeId) != 0) : true) &&
|
||||
(json.find("hopLimit") != json.end() ? json["hopLimit"]->IsNumber() : true) && // hop limit should be a number
|
||||
(json.find("from") != json.end()) && json["from"]->IsNumber() &&
|
||||
(json["from"]->AsNumber() == nodeDB->getNodeNum()) && // only accept message if the "from" is us
|
||||
(json.find("type") != json.end()) && json["type"]->IsString() && // should specify a type
|
||||
(json.find("payload") != json.end()); // should have a payload
|
||||
}
|
||||
|
||||
inline void onReceiveJson(byte *payload, size_t length)
|
||||
{
|
||||
char payloadStr[length + 1];
|
||||
memcpy(payloadStr, payload, length);
|
||||
payloadStr[length] = 0; // null terminated string
|
||||
std::unique_ptr<JSONValue> json_value(JSON::Parse(payloadStr));
|
||||
if (json_value == nullptr) {
|
||||
LOG_ERROR("JSON received payload on MQTT but not a valid JSON");
|
||||
return;
|
||||
}
|
||||
|
||||
JSONObject json;
|
||||
json = json_value->AsObject();
|
||||
|
||||
if (!isValidJsonEnvelope(json)) {
|
||||
LOG_ERROR("JSON received payload on MQTT but not a valid envelope");
|
||||
return;
|
||||
}
|
||||
|
||||
// this is a valid envelope
|
||||
if (json["type"]->AsString().compare("sendtext") == 0 && json["payload"]->IsString()) {
|
||||
std::string jsonPayloadStr = json["payload"]->AsString();
|
||||
LOG_INFO("JSON payload %s, length %u", jsonPayloadStr.c_str(), jsonPayloadStr.length());
|
||||
|
||||
// construct protobuf data packet using TEXT_MESSAGE, send it to the mesh
|
||||
meshtastic_MeshPacket *p = router->allocForSending();
|
||||
p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
||||
if (json.find("channel") != json.end() && json["channel"]->IsNumber() &&
|
||||
(json["channel"]->AsNumber() < channels.getNumChannels()))
|
||||
p->channel = json["channel"]->AsNumber();
|
||||
if (json.find("to") != json.end() && json["to"]->IsNumber())
|
||||
p->to = json["to"]->AsNumber();
|
||||
if (json.find("hopLimit") != json.end() && json["hopLimit"]->IsNumber())
|
||||
p->hop_limit = json["hopLimit"]->AsNumber();
|
||||
if (jsonPayloadStr.length() <= sizeof(p->decoded.payload.bytes)) {
|
||||
memcpy(p->decoded.payload.bytes, jsonPayloadStr.c_str(), jsonPayloadStr.length());
|
||||
p->decoded.payload.size = jsonPayloadStr.length();
|
||||
service->sendToMesh(p, RX_SRC_LOCAL);
|
||||
} else {
|
||||
LOG_WARN("Received MQTT json payload too long, drop");
|
||||
}
|
||||
} else if (json["type"]->AsString().compare("sendposition") == 0 && json["payload"]->IsObject()) {
|
||||
// invent the "sendposition" type for a valid envelope
|
||||
JSONObject posit;
|
||||
posit = json["payload"]->AsObject(); // get nested JSON Position
|
||||
meshtastic_Position pos = meshtastic_Position_init_default;
|
||||
if (posit.find("latitude_i") != posit.end() && posit["latitude_i"]->IsNumber())
|
||||
pos.latitude_i = posit["latitude_i"]->AsNumber();
|
||||
if (posit.find("longitude_i") != posit.end() && posit["longitude_i"]->IsNumber())
|
||||
pos.longitude_i = posit["longitude_i"]->AsNumber();
|
||||
if (posit.find("altitude") != posit.end() && posit["altitude"]->IsNumber())
|
||||
pos.altitude = posit["altitude"]->AsNumber();
|
||||
if (posit.find("time") != posit.end() && posit["time"]->IsNumber())
|
||||
pos.time = posit["time"]->AsNumber();
|
||||
|
||||
// construct protobuf data packet using POSITION, send it to the mesh
|
||||
meshtastic_MeshPacket *p = router->allocForSending();
|
||||
p->decoded.portnum = meshtastic_PortNum_POSITION_APP;
|
||||
if (json.find("channel") != json.end() && json["channel"]->IsNumber() &&
|
||||
(json["channel"]->AsNumber() < channels.getNumChannels()))
|
||||
p->channel = json["channel"]->AsNumber();
|
||||
if (json.find("to") != json.end() && json["to"]->IsNumber())
|
||||
p->to = json["to"]->AsNumber();
|
||||
if (json.find("hopLimit") != json.end() && json["hopLimit"]->IsNumber())
|
||||
p->hop_limit = json["hopLimit"]->AsNumber();
|
||||
p->decoded.payload.size =
|
||||
pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_Position_msg,
|
||||
&pos); // make the Data protobuf from position
|
||||
service->sendToMesh(p, RX_SRC_LOCAL);
|
||||
} else {
|
||||
LOG_DEBUG("JSON ignore downlink message with unsupported type");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Determines if the given IPAddress is a private IPv4 address, i.e. not routable on the public internet.
|
||||
bool isPrivateIpAddress(const IPAddress &ip)
|
||||
{
|
||||
@@ -382,26 +288,6 @@ void MQTT::onReceive(char *topic, byte *payload, size_t length)
|
||||
return;
|
||||
}
|
||||
|
||||
// check if this is a json payload message by comparing the topic start
|
||||
if (moduleConfig.mqtt.json_enabled && (strncmp(topic, jsonTopic.c_str(), jsonTopic.length()) == 0)) {
|
||||
#if !defined(ARCH_NRF52) || NRF52_USE_JSON
|
||||
// parse the channel name from the topic string
|
||||
// the topic has been checked above for having jsonTopic prefix, so just move past it
|
||||
char *channelName = topic + jsonTopic.length();
|
||||
// if another "/" was added, parse string up to that character
|
||||
channelName = strtok(channelName, "/") ? strtok(channelName, "/") : channelName;
|
||||
// We allow downlink JSON packets only on a channel named "mqtt"
|
||||
const meshtastic_Channel &sendChannel = channels.getByName(channelName);
|
||||
if (!(strncasecmp(channels.getGlobalId(sendChannel.index), Channels::mqttChannel, strlen(Channels::mqttChannel)) == 0 &&
|
||||
sendChannel.settings.downlink_enabled)) {
|
||||
LOG_WARN("JSON downlink received on channel not called 'mqtt' or without downlink enabled");
|
||||
return;
|
||||
}
|
||||
onReceiveJson(payload, length);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
onReceiveProto(topic, payload, length);
|
||||
}
|
||||
|
||||
@@ -426,12 +312,10 @@ MQTT::MQTT() : concurrency::OSThread("mqtt"), mqttQueue(MAX_MQTT_QUEUE)
|
||||
|
||||
if (*moduleConfig.mqtt.root) {
|
||||
cryptTopic = moduleConfig.mqtt.root + cryptTopic;
|
||||
jsonTopic = moduleConfig.mqtt.root + jsonTopic;
|
||||
mapTopic = moduleConfig.mqtt.root + mapTopic;
|
||||
isConfiguredForDefaultRootTopic = isDefaultRootTopic(moduleConfig.mqtt.root);
|
||||
} else {
|
||||
cryptTopic = "msh" + cryptTopic;
|
||||
jsonTopic = "msh" + jsonTopic;
|
||||
mapTopic = "msh" + mapTopic;
|
||||
isConfiguredForDefaultRootTopic = true;
|
||||
}
|
||||
@@ -582,14 +466,6 @@ void MQTT::sendSubscriptions()
|
||||
std::string topic = cryptTopic + channels.getGlobalId(i) + "/+";
|
||||
LOG_INFO("Subscribe to %s", topic.c_str());
|
||||
pubSub.subscribe(topic.c_str(), 1); // FIXME, is QOS 1 right?
|
||||
#if !defined(ARCH_NRF52) || \
|
||||
defined(NRF52_USE_JSON) // JSON is not supported on nRF52, see issue #2804 ### Fixed by using ArduinoJSON ###
|
||||
if (moduleConfig.mqtt.json_enabled == true) {
|
||||
std::string topicDecoded = jsonTopic + channels.getGlobalId(i) + "/+";
|
||||
LOG_INFO("Subscribe to %s", topicDecoded.c_str());
|
||||
pubSub.subscribe(topicDecoded.c_str(), 1); // FIXME, is QOS 1 right?
|
||||
}
|
||||
#endif // ARCH_NRF52 NRF52_USE_JSON
|
||||
}
|
||||
}
|
||||
#if !MESHTASTIC_EXCLUDE_PKI
|
||||
@@ -728,33 +604,6 @@ void MQTT::publishQueuedMessages()
|
||||
const std::unique_ptr<QueueEntry> entry(mqttQueue.dequeuePtr(0));
|
||||
LOG_INFO("publish %s, %u bytes from queue", entry->topic.c_str(), entry->envBytes.size());
|
||||
publish(entry->topic.c_str(), entry->envBytes.data(), entry->envBytes.size(), false);
|
||||
|
||||
#if !defined(ARCH_NRF52) || \
|
||||
defined(NRF52_USE_JSON) // JSON is not supported on nRF52, see issue #2804 ### Fixed by using ArduinoJson ###
|
||||
if (!moduleConfig.mqtt.json_enabled)
|
||||
return;
|
||||
|
||||
// handle json topic
|
||||
const DecodedServiceEnvelope env(entry->envBytes.data(), entry->envBytes.size());
|
||||
if (!env.validDecode || env.packet == NULL || env.channel_id == NULL)
|
||||
return;
|
||||
|
||||
auto jsonString = MeshPacketSerializer::JsonSerialize(env.packet);
|
||||
if (jsonString.length() == 0)
|
||||
return;
|
||||
|
||||
// Generate node ID from nodenum for topic
|
||||
std::string nodeId = nodeDB->getNodeId();
|
||||
|
||||
std::string topicJson;
|
||||
if (env.packet->pki_encrypted) {
|
||||
topicJson = jsonTopic + "PKI/" + nodeId;
|
||||
} else {
|
||||
topicJson = jsonTopic + env.channel_id + "/" + nodeId;
|
||||
}
|
||||
LOG_INFO("JSON publish message to %s, %u bytes: %s", topicJson.c_str(), jsonString.length(), jsonString.c_str());
|
||||
publish(topicJson.c_str(), jsonString.c_str(), false);
|
||||
#endif // ARCH_NRF52 NRF52_USE_JSON
|
||||
}
|
||||
|
||||
void MQTT::onSend(const meshtastic_MeshPacket &mp_encrypted, const meshtastic_MeshPacket &mp_decoded, ChannelIndex chIndex)
|
||||
@@ -818,21 +667,6 @@ void MQTT::onSend(const meshtastic_MeshPacket &mp_encrypted, const meshtastic_Me
|
||||
if (moduleConfig.mqtt.proxy_to_client_enabled || this->isConnectedDirectly()) {
|
||||
LOG_DEBUG("MQTT Publish %s, %u bytes", topic.c_str(), numBytes);
|
||||
publish(topic.c_str(), bytes, numBytes, false);
|
||||
|
||||
#if !defined(ARCH_NRF52) || \
|
||||
defined(NRF52_USE_JSON) // JSON is not supported on nRF52, see issue #2804 ### Fixed by using ArduinoJson ###
|
||||
if (!moduleConfig.mqtt.json_enabled)
|
||||
return;
|
||||
// handle json topic
|
||||
auto jsonString = MeshPacketSerializer::JsonSerialize(&mp_decoded);
|
||||
if (jsonString.length() == 0)
|
||||
return;
|
||||
// Generate node ID from nodenum for JSON topic
|
||||
std::string nodeIdForJson = nodeDB->getNodeId();
|
||||
std::string topicJson = jsonTopic + channelId + "/" + nodeIdForJson;
|
||||
LOG_INFO("JSON publish message to %s, %u bytes: %s", topicJson.c_str(), jsonString.length(), jsonString.c_str());
|
||||
publish(topicJson.c_str(), jsonString.c_str(), false);
|
||||
#endif // ARCH_NRF52 NRF52_USE_JSON
|
||||
} else {
|
||||
LOG_INFO("MQTT not connected, queue packet");
|
||||
QueueEntry *entry;
|
||||
|
||||
+2
-6
@@ -6,9 +6,6 @@
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "mesh/Channels.h"
|
||||
#include "mesh/generated/meshtastic/mqtt.pb.h"
|
||||
#if !defined(ARCH_NRF52) || NRF52_USE_JSON
|
||||
#include "serialization/JSON.h"
|
||||
#endif
|
||||
#if HAS_WIFI
|
||||
#include <WiFiClient.h>
|
||||
#if __has_include(<WiFiClientSecure.h>)
|
||||
@@ -101,9 +98,8 @@ class MQTT : private concurrency::OSThread
|
||||
explicit MQTT(std::unique_ptr<MQTTClient> mqttClient);
|
||||
#endif
|
||||
|
||||
std::string cryptTopic = "/2/e/"; // msh/2/e/CHANNELID/NODEID
|
||||
std::string jsonTopic = "/2/json/"; // msh/2/json/CHANNELID/NODEID
|
||||
std::string mapTopic = "/2/map/"; // For protobuf-encoded MapReport messages
|
||||
std::string cryptTopic = "/2/e/"; // msh/2/e/CHANNELID/NODEID
|
||||
std::string mapTopic = "/2/map/"; // For protobuf-encoded MapReport messages
|
||||
|
||||
// For map reporting (only applies when enabled)
|
||||
const uint32_t default_map_position_precision = 14; // defaults to max. offset of ~1459m
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <nrfx_wdt.h>
|
||||
#include <stdio.h>
|
||||
// #include <Adafruit_USBD_Device.h>
|
||||
#include "HardwareRNG.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerMon.h"
|
||||
#include "error.h"
|
||||
@@ -398,15 +399,14 @@ void nrf52Setup()
|
||||
#endif
|
||||
|
||||
// Init random seed
|
||||
union seedParts {
|
||||
uint32_t seed32;
|
||||
uint8_t seed8[4];
|
||||
} seed;
|
||||
nRFCrypto.begin();
|
||||
nRFCrypto.Random.generate(seed.seed8, sizeof(seed.seed8));
|
||||
LOG_DEBUG("Set random seed %u", seed.seed32);
|
||||
randomSeed(seed.seed32);
|
||||
nRFCrypto.end();
|
||||
uint32_t seed = 0;
|
||||
if (!HardwareRNG::seed(seed)) {
|
||||
LOG_WARN("Hardware RNG seed unavailable, using PRNG fallback");
|
||||
// Use a hardware timer value as a fallback seed for better entropy
|
||||
seed = micros();
|
||||
}
|
||||
LOG_DEBUG("Set random seed %u", seed);
|
||||
randomSeed(seed);
|
||||
|
||||
// Set up nrfx watchdog. Do not enable the watchdog yet (we do that
|
||||
// the first time through the main loop), so that other threads can
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "CryptoEngine.h"
|
||||
#include "HardwareRNG.h"
|
||||
#include "PortduinoGPIO.h"
|
||||
#include "SPIChip.h"
|
||||
#include "mesh/RF95Interface.h"
|
||||
@@ -181,6 +182,10 @@ void portduinoSetup()
|
||||
// Force stdout to be line buffered
|
||||
setvbuf(stdout, stdoutBuffer, _IOLBF, sizeof(stdoutBuffer));
|
||||
|
||||
// We do this super early so that we can log from the rest of the init code
|
||||
concurrency::hasBeenSetup = true;
|
||||
consoleInit();
|
||||
|
||||
if (portduino_config.force_simradio == true) {
|
||||
portduino_config.lora_module = use_simradio;
|
||||
} else if (configPath != nullptr) {
|
||||
@@ -233,7 +238,9 @@ void portduinoSetup()
|
||||
std::cout << "Running in simulated mode." << std::endl;
|
||||
portduino_config.MaxNodes = 200; // Default to 200 nodes
|
||||
// Set the random seed equal to TCPPort to have a different seed per instance
|
||||
randomSeed(TCPPort);
|
||||
uint32_t seed = TCPPort;
|
||||
HardwareRNG::seed(seed);
|
||||
randomSeed(seed);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -512,7 +519,9 @@ void portduinoSetup()
|
||||
#endif
|
||||
printf("MAC ADDRESS: %02X:%02X:%02X:%02X:%02X:%02X\n", dmac[0], dmac[1], dmac[2], dmac[3], dmac[4], dmac[5]);
|
||||
// Rather important to set this, if not running simulated.
|
||||
randomSeed(time(NULL));
|
||||
uint32_t seed = static_cast<uint32_t>(time(NULL));
|
||||
HardwareRNG::seed(seed);
|
||||
randomSeed(seed);
|
||||
|
||||
std::string defaultGpioChipName = gpioChipName + std::to_string(portduino_config.lora_default_gpiochip);
|
||||
|
||||
@@ -645,7 +654,9 @@ void portduinoSetup()
|
||||
if (verboseEnabled && portduino_config.logoutputlevel != level_trace) {
|
||||
portduino_config.logoutputlevel = level_debug;
|
||||
}
|
||||
|
||||
if (portduino_config.lora_spi_dev != "") {
|
||||
portduinoSetOptions({.realHardware = true});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#include "HardwareRNG.h"
|
||||
#include "configuration.h"
|
||||
#include "hardware/xosc.h"
|
||||
#include <hardware/clocks.h>
|
||||
@@ -98,10 +99,12 @@ void getMacAddr(uint8_t *dmac)
|
||||
|
||||
void rp2040Setup()
|
||||
{
|
||||
/* Sets a random seed to make sure we get different random numbers on each boot.
|
||||
Taken from CPU cycle counter and ROSC oscillator, so should be pretty random.
|
||||
*/
|
||||
randomSeed(rp2040.hwrand32());
|
||||
/* Sets a random seed to make sure we get different random numbers on each boot. */
|
||||
uint32_t seed = 0;
|
||||
if (!HardwareRNG::seed(seed)) {
|
||||
seed = rp2040.hwrand32();
|
||||
}
|
||||
randomSeed(seed);
|
||||
|
||||
#ifdef RP2040_SLOW_CLOCK
|
||||
uint f_pll_sys = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_PLL_SYS_CLKSRC_PRIMARY);
|
||||
|
||||
@@ -4,6 +4,57 @@
|
||||
#include <stm32wle5xx.h>
|
||||
#include <stm32wlxx_hal.h>
|
||||
|
||||
// ─── Bootloader redirect ──────────────────────────────────────────────────────
|
||||
//
|
||||
// Why .noinit + constructor instead of TAMP backup registers:
|
||||
//
|
||||
// The STM32duino startup sequence initialises clocks which may call
|
||||
// __HAL_RCC_BACKUPRESET_FORCE/RELEASE when configuring the LSE oscillator,
|
||||
// wiping the entire backup domain (including TAMP->BKP0R) before setup()
|
||||
// ever runs. The backup-register approach therefore cannot reliably survive
|
||||
// a soft reset in this toolchain.
|
||||
//
|
||||
// Solution: store the magic in a .noinit SRAM variable.
|
||||
// - NVIC_SystemReset() does NOT clear SRAM.
|
||||
// - The linker script skips zero-init for .noinit sections.
|
||||
// - __attribute__((constructor)) fires before main()/HAL_Init(), so we can
|
||||
// intercept and jump before anything disturbs peripheral state.
|
||||
|
||||
#define BOOTLOADER_MAGIC 0xD00DB007UL
|
||||
#define SYS_MEM_BASE 0x1FFF0000UL
|
||||
|
||||
// Placed in .noinit — not zeroed at startup, survives NVIC_SystemReset().
|
||||
__attribute__((section(".noinit"), used)) volatile uint32_t g_bootloaderMagic;
|
||||
|
||||
// Fires before main() / HAL_Init(). Must use only core Cortex-M registers.
|
||||
__attribute__((constructor(101), used)) static void earlyBootCheck(void)
|
||||
{
|
||||
if (g_bootloaderMagic != BOOTLOADER_MAGIC)
|
||||
return;
|
||||
g_bootloaderMagic = 0;
|
||||
|
||||
SysTick->CTRL = 0;
|
||||
SysTick->LOAD = 0;
|
||||
SysTick->VAL = 0;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
NVIC->ICER[i] = 0xFFFFFFFF;
|
||||
NVIC->ICPR[i] = 0xFFFFFFFF;
|
||||
}
|
||||
__DSB();
|
||||
__ISB();
|
||||
SCB->VTOR = SYS_MEM_BASE;
|
||||
__set_MSP(*(volatile uint32_t *)SYS_MEM_BASE);
|
||||
((void (*)(void))(*(volatile uint32_t *)(SYS_MEM_BASE + 4)))();
|
||||
while (1)
|
||||
;
|
||||
}
|
||||
|
||||
void enterDfuMode()
|
||||
{
|
||||
g_bootloaderMagic = BOOTLOADER_MAGIC;
|
||||
HAL_NVIC_SystemReset();
|
||||
}
|
||||
|
||||
void setBluetoothEnable(bool enable) {}
|
||||
|
||||
void playStartMelody() {}
|
||||
|
||||
@@ -15,8 +15,13 @@
|
||||
|
||||
// Device specific curves go in variant.h
|
||||
#ifndef OCV_ARRAY
|
||||
#if defined(ARCH_STM32WL) && BATTERY_PIN == AVBAT
|
||||
// STM32 VDD/VBAT absolute maximum is 4V so use an LFP curve
|
||||
#define OCV_ARRAY 3650, 3400, 3340, 3320, 3300, 3280, 3270, 3260, 3240, 3200, 2500
|
||||
#else
|
||||
#define OCV_ARRAY 4190, 4050, 3990, 3890, 3800, 3720, 3630, 3530, 3420, 3300, 3100
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/*Note: 12V lead acid is 6 cells, most board accept only 1 cell LiIon/LiPo*/
|
||||
#ifndef NUM_CELLS
|
||||
|
||||
@@ -1,245 +0,0 @@
|
||||
/*
|
||||
* File JSON.cpp part of the SimpleJSON Library - http://mjpa.in/json
|
||||
*
|
||||
* Copyright (C) 2010 Mike Anchor
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "JSON.h"
|
||||
|
||||
/**
|
||||
* Blocks off the public constructor
|
||||
*
|
||||
* @access private
|
||||
*
|
||||
*/
|
||||
JSON::JSON() {}
|
||||
|
||||
/**
|
||||
* Parses a complete JSON encoded string
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param char* data The JSON text
|
||||
*
|
||||
* @return JSONValue* Returns a JSON Value representing the root, or NULL on error
|
||||
*/
|
||||
JSONValue *JSON::Parse(const char *data)
|
||||
{
|
||||
// Skip any preceding whitespace, end of data = no JSON = fail
|
||||
if (!SkipWhitespace(&data))
|
||||
return NULL;
|
||||
|
||||
// We need the start of a value here now...
|
||||
JSONValue *value = JSONValue::Parse(&data);
|
||||
if (value == NULL)
|
||||
return NULL;
|
||||
|
||||
// Can be white space now and should be at the end of the string then...
|
||||
if (SkipWhitespace(&data)) {
|
||||
delete value;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// We're now at the end of the string
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns the passed in JSONValue into a JSON encode string
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param JSONValue* value The root value
|
||||
*
|
||||
* @return std::string Returns a JSON encoded string representation of the given value
|
||||
*/
|
||||
std::string JSON::Stringify(const JSONValue *value)
|
||||
{
|
||||
if (value != NULL)
|
||||
return value->Stringify();
|
||||
else
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips over any whitespace characters (space, tab, \r or \n) defined by the JSON spec
|
||||
*
|
||||
* @access protected
|
||||
*
|
||||
* @param char** data Pointer to a char* that contains the JSON text
|
||||
*
|
||||
* @return bool Returns true if there is more data, or false if the end of the text was reached
|
||||
*/
|
||||
bool JSON::SkipWhitespace(const char **data)
|
||||
{
|
||||
while (**data != 0 && (**data == ' ' || **data == '\t' || **data == '\r' || **data == '\n'))
|
||||
(*data)++;
|
||||
|
||||
return **data != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a JSON String as defined by the spec - "<some chars>"
|
||||
* Any escaped characters are swapped out for their unescaped values
|
||||
*
|
||||
* @access protected
|
||||
*
|
||||
* @param char** data Pointer to a char* that contains the JSON text
|
||||
* @param std::string& str Reference to a std::string to receive the extracted string
|
||||
*
|
||||
* @return bool Returns true on success, false on failure
|
||||
*/
|
||||
bool JSON::ExtractString(const char **data, std::string &str)
|
||||
{
|
||||
str = "";
|
||||
|
||||
while (**data != 0) {
|
||||
// Save the char so we can change it if need be
|
||||
char next_char = **data;
|
||||
|
||||
// Escaping something?
|
||||
if (next_char == '\\') {
|
||||
// Move over the escape char
|
||||
(*data)++;
|
||||
|
||||
// Deal with the escaped char
|
||||
switch (**data) {
|
||||
case '"':
|
||||
next_char = '"';
|
||||
break;
|
||||
case '\\':
|
||||
next_char = '\\';
|
||||
break;
|
||||
case '/':
|
||||
next_char = '/';
|
||||
break;
|
||||
case 'b':
|
||||
next_char = '\b';
|
||||
break;
|
||||
case 'f':
|
||||
next_char = '\f';
|
||||
break;
|
||||
case 'n':
|
||||
next_char = '\n';
|
||||
break;
|
||||
case 'r':
|
||||
next_char = '\r';
|
||||
break;
|
||||
case 't':
|
||||
next_char = '\t';
|
||||
break;
|
||||
case 'u': {
|
||||
// We need 5 chars (4 hex + the 'u') or its not valid
|
||||
if (!simplejson_csnlen(*data, 5))
|
||||
return false;
|
||||
|
||||
// Deal with the chars
|
||||
next_char = 0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
// Do it first to move off the 'u' and leave us on the
|
||||
// final hex digit as we move on by one later on
|
||||
(*data)++;
|
||||
|
||||
next_char <<= 4;
|
||||
|
||||
// Parse the hex digit
|
||||
if (**data >= '0' && **data <= '9')
|
||||
next_char |= (**data - '0');
|
||||
else if (**data >= 'A' && **data <= 'F')
|
||||
next_char |= (10 + (**data - 'A'));
|
||||
else if (**data >= 'a' && **data <= 'f')
|
||||
next_char |= (10 + (**data - 'a'));
|
||||
else {
|
||||
// Invalid hex digit = invalid JSON
|
||||
return false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// By the spec, only the above cases are allowed
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// End of the string?
|
||||
else if (next_char == '"') {
|
||||
(*data)++;
|
||||
str.shrink_to_fit(); // Remove unused capacity
|
||||
return true;
|
||||
}
|
||||
|
||||
// Disallowed char?
|
||||
else if (next_char < ' ' && next_char != '\t') {
|
||||
// SPEC Violation: Allow tabs due to real world cases
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add the next char
|
||||
str += next_char;
|
||||
|
||||
// Move on
|
||||
(*data)++;
|
||||
}
|
||||
|
||||
// If we're here, the string ended incorrectly
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses some text as though it is an integer
|
||||
*
|
||||
* @access protected
|
||||
*
|
||||
* @param char** data Pointer to a char* that contains the JSON text
|
||||
*
|
||||
* @return double Returns the double value of the number found
|
||||
*/
|
||||
double JSON::ParseInt(const char **data)
|
||||
{
|
||||
double integer = 0;
|
||||
while (**data != 0 && **data >= '0' && **data <= '9')
|
||||
integer = integer * 10 + (*(*data)++ - '0');
|
||||
|
||||
return integer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses some text as though it is a decimal
|
||||
*
|
||||
* @access protected
|
||||
*
|
||||
* @param char** data Pointer to a char* that contains the JSON text
|
||||
*
|
||||
* @return double Returns the double value of the decimal found
|
||||
*/
|
||||
double JSON::ParseDecimal(const char **data)
|
||||
{
|
||||
double decimal = 0.0;
|
||||
double factor = 0.1;
|
||||
while (**data != 0 && **data >= '0' && **data <= '9') {
|
||||
int digit = (*(*data)++ - '0');
|
||||
decimal = decimal + digit * factor;
|
||||
factor *= 0.1;
|
||||
}
|
||||
return decimal;
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
* File JSON.h part of the SimpleJSON Library - http://mjpa.in/json
|
||||
*
|
||||
* Copyright (C) 2010 Mike Anchor
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef _JSON_H_
|
||||
#define _JSON_H_
|
||||
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Simple function to check a string 's' has at least 'n' characters
|
||||
static inline bool simplejson_csnlen(const char *s, size_t n)
|
||||
{
|
||||
if (s == 0)
|
||||
return false;
|
||||
|
||||
const char *save = s;
|
||||
while (n-- > 0) {
|
||||
if (*(save++) == 0)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Custom types
|
||||
class JSONValue;
|
||||
typedef std::vector<JSONValue *> JSONArray;
|
||||
typedef std::map<std::string, JSONValue *> JSONObject;
|
||||
|
||||
#include "JSONValue.h"
|
||||
|
||||
class JSON
|
||||
{
|
||||
friend class JSONValue;
|
||||
|
||||
public:
|
||||
static JSONValue *Parse(const char *data);
|
||||
static std::string Stringify(const JSONValue *value);
|
||||
|
||||
protected:
|
||||
static bool SkipWhitespace(const char **data);
|
||||
static bool ExtractString(const char **data, std::string &str);
|
||||
static double ParseInt(const char **data);
|
||||
static double ParseDecimal(const char **data);
|
||||
|
||||
private:
|
||||
JSON();
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -1,897 +0,0 @@
|
||||
/*
|
||||
* File JSONValue.cpp part of the SimpleJSON Library - http://mjpa.in/json
|
||||
*
|
||||
* Copyright (C) 2010 Mike Anchor
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include <math.h>
|
||||
#include <sstream>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "JSONValue.h"
|
||||
|
||||
// Macros to free an array/object
|
||||
#define FREE_ARRAY(x) \
|
||||
{ \
|
||||
JSONArray::iterator iter; \
|
||||
for (iter = x.begin(); iter != x.end(); ++iter) { \
|
||||
delete *iter; \
|
||||
} \
|
||||
}
|
||||
#define FREE_OBJECT(x) \
|
||||
{ \
|
||||
JSONObject::iterator iter; \
|
||||
for (iter = x.begin(); iter != x.end(); ++iter) { \
|
||||
delete (*iter).second; \
|
||||
} \
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a JSON encoded value to a JSONValue object
|
||||
*
|
||||
* @access protected
|
||||
*
|
||||
* @param char** data Pointer to a char* that contains the data
|
||||
*
|
||||
* @return JSONValue* Returns a pointer to a JSONValue object on success, NULL on error
|
||||
*/
|
||||
JSONValue *JSONValue::Parse(const char **data)
|
||||
{
|
||||
// Is it a string?
|
||||
if (**data == '"') {
|
||||
std::string str;
|
||||
if (!JSON::ExtractString(&(++(*data)), str))
|
||||
return NULL;
|
||||
else
|
||||
return new JSONValue(str);
|
||||
}
|
||||
|
||||
// Is it a boolean?
|
||||
else if ((simplejson_csnlen(*data, 4) && strncasecmp(*data, "true", 4) == 0) ||
|
||||
(simplejson_csnlen(*data, 5) && strncasecmp(*data, "false", 5) == 0)) {
|
||||
bool value = strncasecmp(*data, "true", 4) == 0;
|
||||
(*data) += value ? 4 : 5;
|
||||
return new JSONValue(value);
|
||||
}
|
||||
|
||||
// Is it a null?
|
||||
else if (simplejson_csnlen(*data, 4) && strncasecmp(*data, "null", 4) == 0) {
|
||||
(*data) += 4;
|
||||
return new JSONValue();
|
||||
}
|
||||
|
||||
// Is it a number?
|
||||
else if (**data == '-' || (**data >= '0' && **data <= '9')) {
|
||||
// Negative?
|
||||
bool neg = **data == '-';
|
||||
if (neg)
|
||||
(*data)++;
|
||||
|
||||
double number = 0.0;
|
||||
|
||||
// Parse the whole part of the number - only if it wasn't 0
|
||||
if (**data == '0')
|
||||
(*data)++;
|
||||
else if (**data >= '1' && **data <= '9')
|
||||
number = JSON::ParseInt(data);
|
||||
else
|
||||
return NULL;
|
||||
|
||||
// Could be a decimal now...
|
||||
if (**data == '.') {
|
||||
(*data)++;
|
||||
|
||||
// Not get any digits?
|
||||
if (!(**data >= '0' && **data <= '9'))
|
||||
return NULL;
|
||||
|
||||
// Find the decimal and sort the decimal place out
|
||||
// Use ParseDecimal as ParseInt won't work with decimals less than 0.1
|
||||
// thanks to Javier Abadia for the report & fix
|
||||
double decimal = JSON::ParseDecimal(data);
|
||||
|
||||
// Save the number
|
||||
number += decimal;
|
||||
}
|
||||
|
||||
// Could be an exponent now...
|
||||
if (**data == 'E' || **data == 'e') {
|
||||
(*data)++;
|
||||
|
||||
// Check signage of expo
|
||||
bool neg_expo = false;
|
||||
if (**data == '-' || **data == '+') {
|
||||
neg_expo = **data == '-';
|
||||
(*data)++;
|
||||
}
|
||||
|
||||
// Not get any digits?
|
||||
if (!(**data >= '0' && **data <= '9'))
|
||||
return NULL;
|
||||
|
||||
// Sort the expo out
|
||||
double expo = JSON::ParseInt(data);
|
||||
for (double i = 0.0; i < expo; i++)
|
||||
number = neg_expo ? (number / 10.0) : (number * 10.0);
|
||||
}
|
||||
|
||||
// Was it neg?
|
||||
if (neg)
|
||||
number *= -1;
|
||||
|
||||
return new JSONValue(number);
|
||||
}
|
||||
|
||||
// An object?
|
||||
else if (**data == '{') {
|
||||
JSONObject object;
|
||||
|
||||
(*data)++;
|
||||
|
||||
while (**data != 0) {
|
||||
// Whitespace at the start?
|
||||
if (!JSON::SkipWhitespace(data)) {
|
||||
FREE_OBJECT(object);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Special case - empty object
|
||||
if (object.size() == 0 && **data == '}') {
|
||||
(*data)++;
|
||||
return new JSONValue(object);
|
||||
}
|
||||
|
||||
// We want a string now...
|
||||
std::string name;
|
||||
if (!JSON::ExtractString(&(++(*data)), name)) {
|
||||
FREE_OBJECT(object);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// More whitespace?
|
||||
if (!JSON::SkipWhitespace(data)) {
|
||||
FREE_OBJECT(object);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Need a : now
|
||||
if (*((*data)++) != ':') {
|
||||
FREE_OBJECT(object);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// More whitespace?
|
||||
if (!JSON::SkipWhitespace(data)) {
|
||||
FREE_OBJECT(object);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// The value is here
|
||||
JSONValue *value = Parse(data);
|
||||
if (value == NULL) {
|
||||
FREE_OBJECT(object);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Add the name:value
|
||||
if (object.find(name) != object.end())
|
||||
delete object[name];
|
||||
object[name] = value;
|
||||
|
||||
// More whitespace?
|
||||
if (!JSON::SkipWhitespace(data)) {
|
||||
FREE_OBJECT(object);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// End of object?
|
||||
if (**data == '}') {
|
||||
(*data)++;
|
||||
return new JSONValue(object);
|
||||
}
|
||||
|
||||
// Want a , now
|
||||
if (**data != ',') {
|
||||
FREE_OBJECT(object);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
(*data)++;
|
||||
}
|
||||
|
||||
// Only here if we ran out of data
|
||||
FREE_OBJECT(object);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// An array?
|
||||
else if (**data == '[') {
|
||||
JSONArray array;
|
||||
|
||||
(*data)++;
|
||||
|
||||
while (**data != 0) {
|
||||
// Whitespace at the start?
|
||||
if (!JSON::SkipWhitespace(data)) {
|
||||
FREE_ARRAY(array);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Special case - empty array
|
||||
if (array.size() == 0 && **data == ']') {
|
||||
(*data)++;
|
||||
return new JSONValue(array);
|
||||
}
|
||||
|
||||
// Get the value
|
||||
JSONValue *value = Parse(data);
|
||||
if (value == NULL) {
|
||||
FREE_ARRAY(array);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Add the value
|
||||
array.push_back(value);
|
||||
|
||||
// More whitespace?
|
||||
if (!JSON::SkipWhitespace(data)) {
|
||||
FREE_ARRAY(array);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// End of array?
|
||||
if (**data == ']') {
|
||||
(*data)++;
|
||||
return new JSONValue(array);
|
||||
}
|
||||
|
||||
// Want a , now
|
||||
if (**data != ',') {
|
||||
FREE_ARRAY(array);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
(*data)++;
|
||||
}
|
||||
|
||||
// Only here if we ran out of data
|
||||
FREE_ARRAY(array);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Ran out of possibilities, it's bad!
|
||||
else {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic constructor for creating a JSON Value of type NULL
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
JSONValue::JSONValue(/*NULL*/)
|
||||
{
|
||||
type = JSONType_Null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic constructor for creating a JSON Value of type String
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param char* m_char_value The string to use as the value
|
||||
*/
|
||||
JSONValue::JSONValue(const char *m_char_value)
|
||||
{
|
||||
type = JSONType_String;
|
||||
string_value = new std::string(std::string(m_char_value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic constructor for creating a JSON Value of type String
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param std::string m_string_value The string to use as the value
|
||||
*/
|
||||
JSONValue::JSONValue(const std::string &m_string_value)
|
||||
{
|
||||
type = JSONType_String;
|
||||
string_value = new std::string(m_string_value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic constructor for creating a JSON Value of type Bool
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param bool m_bool_value The bool to use as the value
|
||||
*/
|
||||
JSONValue::JSONValue(bool m_bool_value)
|
||||
{
|
||||
type = JSONType_Bool;
|
||||
bool_value = m_bool_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic constructor for creating a JSON Value of type Number
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param double m_number_value The number to use as the value
|
||||
*/
|
||||
JSONValue::JSONValue(double m_number_value)
|
||||
{
|
||||
type = JSONType_Number;
|
||||
number_value = m_number_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic constructor for creating a JSON Value of type Number
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param int m_integer_value The number to use as the value
|
||||
*/
|
||||
JSONValue::JSONValue(int m_integer_value)
|
||||
{
|
||||
type = JSONType_Number;
|
||||
number_value = (double)m_integer_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic constructor for creating a JSON Value of type Number
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param unsigned int m_integer_value The number to use as the value
|
||||
*/
|
||||
JSONValue::JSONValue(unsigned int m_integer_value)
|
||||
{
|
||||
type = JSONType_Number;
|
||||
number_value = (double)m_integer_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic constructor for creating a JSON Value of type Array
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param JSONArray m_array_value The JSONArray to use as the value
|
||||
*/
|
||||
JSONValue::JSONValue(const JSONArray &m_array_value)
|
||||
{
|
||||
type = JSONType_Array;
|
||||
array_value = new JSONArray(m_array_value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic constructor for creating a JSON Value of type Object
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param JSONObject m_object_value The JSONObject to use as the value
|
||||
*/
|
||||
JSONValue::JSONValue(const JSONObject &m_object_value)
|
||||
{
|
||||
type = JSONType_Object;
|
||||
object_value = new JSONObject(m_object_value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy constructor to perform a deep copy of array / object values
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param JSONValue m_source The source JSONValue that is being copied
|
||||
*/
|
||||
JSONValue::JSONValue(const JSONValue &m_source)
|
||||
{
|
||||
type = m_source.type;
|
||||
|
||||
switch (type) {
|
||||
case JSONType_String:
|
||||
string_value = new std::string(*m_source.string_value);
|
||||
break;
|
||||
|
||||
case JSONType_Bool:
|
||||
bool_value = m_source.bool_value;
|
||||
break;
|
||||
|
||||
case JSONType_Number:
|
||||
number_value = m_source.number_value;
|
||||
break;
|
||||
|
||||
case JSONType_Array: {
|
||||
JSONArray source_array = *m_source.array_value;
|
||||
JSONArray::iterator iter;
|
||||
array_value = new JSONArray();
|
||||
for (iter = source_array.begin(); iter != source_array.end(); ++iter)
|
||||
array_value->push_back(new JSONValue(**iter));
|
||||
break;
|
||||
}
|
||||
|
||||
case JSONType_Object: {
|
||||
JSONObject source_object = *m_source.object_value;
|
||||
object_value = new JSONObject();
|
||||
JSONObject::iterator iter;
|
||||
for (iter = source_object.begin(); iter != source_object.end(); ++iter) {
|
||||
std::string name = (*iter).first;
|
||||
(*object_value)[name] = new JSONValue(*((*iter).second));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case JSONType_Null:
|
||||
// Nothing to do.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The destructor for the JSON Value object
|
||||
* Handles deleting the objects in the array or the object value
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
JSONValue::~JSONValue()
|
||||
{
|
||||
if (type == JSONType_Array) {
|
||||
JSONArray::iterator iter;
|
||||
for (iter = array_value->begin(); iter != array_value->end(); ++iter)
|
||||
delete *iter;
|
||||
delete array_value;
|
||||
} else if (type == JSONType_Object) {
|
||||
JSONObject::iterator iter;
|
||||
for (iter = object_value->begin(); iter != object_value->end(); ++iter) {
|
||||
delete (*iter).second;
|
||||
}
|
||||
delete object_value;
|
||||
} else if (type == JSONType_String) {
|
||||
delete string_value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the value is a NULL
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return bool Returns true if it is a NULL value, false otherwise
|
||||
*/
|
||||
bool JSONValue::IsNull() const
|
||||
{
|
||||
return type == JSONType_Null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the value is a String
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return bool Returns true if it is a String value, false otherwise
|
||||
*/
|
||||
bool JSONValue::IsString() const
|
||||
{
|
||||
return type == JSONType_String;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the value is a Bool
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return bool Returns true if it is a Bool value, false otherwise
|
||||
*/
|
||||
bool JSONValue::IsBool() const
|
||||
{
|
||||
return type == JSONType_Bool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the value is a Number
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return bool Returns true if it is a Number value, false otherwise
|
||||
*/
|
||||
bool JSONValue::IsNumber() const
|
||||
{
|
||||
return type == JSONType_Number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the value is an Array
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return bool Returns true if it is an Array value, false otherwise
|
||||
*/
|
||||
bool JSONValue::IsArray() const
|
||||
{
|
||||
return type == JSONType_Array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the value is an Object
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return bool Returns true if it is an Object value, false otherwise
|
||||
*/
|
||||
bool JSONValue::IsObject() const
|
||||
{
|
||||
return type == JSONType_Object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the String value of this JSONValue
|
||||
* Use IsString() before using this method.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return std::string Returns the string value
|
||||
*/
|
||||
const std::string &JSONValue::AsString() const
|
||||
{
|
||||
return (*string_value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the Bool value of this JSONValue
|
||||
* Use IsBool() before using this method.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return bool Returns the bool value
|
||||
*/
|
||||
bool JSONValue::AsBool() const
|
||||
{
|
||||
return bool_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the Number value of this JSONValue
|
||||
* Use IsNumber() before using this method.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return double Returns the number value
|
||||
*/
|
||||
double JSONValue::AsNumber() const
|
||||
{
|
||||
return number_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the Array value of this JSONValue
|
||||
* Use IsArray() before using this method.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return JSONArray Returns the array value
|
||||
*/
|
||||
const JSONArray &JSONValue::AsArray() const
|
||||
{
|
||||
return (*array_value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the Object value of this JSONValue
|
||||
* Use IsObject() before using this method.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return JSONObject Returns the object value
|
||||
*/
|
||||
const JSONObject &JSONValue::AsObject() const
|
||||
{
|
||||
return (*object_value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the number of children of this JSONValue.
|
||||
* This number will be 0 or the actual number of children
|
||||
* if IsArray() or IsObject().
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return The number of children.
|
||||
*/
|
||||
std::size_t JSONValue::CountChildren() const
|
||||
{
|
||||
switch (type) {
|
||||
case JSONType_Array:
|
||||
return array_value->size();
|
||||
case JSONType_Object:
|
||||
return object_value->size();
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this JSONValue has a child at the given index.
|
||||
* Use IsArray() before using this method.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return bool Returns true if the array has a value at the given index.
|
||||
*/
|
||||
bool JSONValue::HasChild(std::size_t index) const
|
||||
{
|
||||
if (type == JSONType_Array) {
|
||||
return index < array_value->size();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the child of this JSONValue at the given index.
|
||||
* Use IsArray() before using this method.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return JSONValue* Returns JSONValue at the given index or NULL
|
||||
* if it doesn't exist.
|
||||
*/
|
||||
JSONValue *JSONValue::Child(std::size_t index)
|
||||
{
|
||||
if (index < array_value->size()) {
|
||||
return (*array_value)[index];
|
||||
} else {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this JSONValue has a child at the given key.
|
||||
* Use IsObject() before using this method.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return bool Returns true if the object has a value at the given key.
|
||||
*/
|
||||
bool JSONValue::HasChild(const char *name) const
|
||||
{
|
||||
if (type == JSONType_Object) {
|
||||
return object_value->find(name) != object_value->end();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the child of this JSONValue at the given key.
|
||||
* Use IsObject() before using this method.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return JSONValue* Returns JSONValue for the given key in the object
|
||||
* or NULL if it doesn't exist.
|
||||
*/
|
||||
JSONValue *JSONValue::Child(const char *name)
|
||||
{
|
||||
JSONObject::const_iterator it = object_value->find(name);
|
||||
if (it != object_value->end()) {
|
||||
return it->second;
|
||||
} else {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the keys of the JSON Object or an empty vector
|
||||
* if this value is not an object.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return std::vector<std::string> A vector containing the keys.
|
||||
*/
|
||||
std::vector<std::string> JSONValue::ObjectKeys() const
|
||||
{
|
||||
std::vector<std::string> keys;
|
||||
|
||||
if (type == JSONType_Object) {
|
||||
JSONObject::const_iterator iter = object_value->begin();
|
||||
while (iter != object_value->end()) {
|
||||
keys.push_back(iter->first);
|
||||
|
||||
++iter;
|
||||
}
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a JSON encoded string for the value with all necessary characters escaped
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param bool prettyprint Enable prettyprint
|
||||
*
|
||||
* @return std::string Returns the JSON string
|
||||
*/
|
||||
std::string JSONValue::Stringify(bool const prettyprint) const
|
||||
{
|
||||
size_t const indentDepth = prettyprint ? 1 : 0;
|
||||
return StringifyImpl(indentDepth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a JSON encoded string for the value with all necessary characters escaped
|
||||
*
|
||||
* @access private
|
||||
*
|
||||
* @param size_t indentDepth The prettyprint indentation depth (0 : no prettyprint)
|
||||
*
|
||||
* @return std::string Returns the JSON string
|
||||
*/
|
||||
std::string JSONValue::StringifyImpl(size_t const indentDepth) const
|
||||
{
|
||||
std::string ret_string;
|
||||
size_t const indentDepth1 = indentDepth ? indentDepth + 1 : 0;
|
||||
std::string const indentStr = Indent(indentDepth);
|
||||
std::string const indentStr1 = Indent(indentDepth1);
|
||||
|
||||
switch (type) {
|
||||
case JSONType_Null:
|
||||
ret_string = "null";
|
||||
break;
|
||||
|
||||
case JSONType_String:
|
||||
ret_string = StringifyString(*string_value);
|
||||
break;
|
||||
|
||||
case JSONType_Bool:
|
||||
ret_string = bool_value ? "true" : "false";
|
||||
break;
|
||||
|
||||
case JSONType_Number: {
|
||||
if (isinf(number_value) || isnan(number_value))
|
||||
ret_string = "null";
|
||||
else {
|
||||
std::stringstream ss;
|
||||
ss.precision(15);
|
||||
ss << number_value;
|
||||
ret_string = ss.str();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case JSONType_Array: {
|
||||
ret_string = indentDepth ? "[\n" + indentStr1 : "[";
|
||||
JSONArray::const_iterator iter = array_value->begin();
|
||||
while (iter != array_value->end()) {
|
||||
ret_string += (*iter)->StringifyImpl(indentDepth1);
|
||||
|
||||
// Not at the end - add a separator
|
||||
if (++iter != array_value->end())
|
||||
ret_string += ",";
|
||||
}
|
||||
ret_string += indentDepth ? "\n" + indentStr + "]" : "]";
|
||||
break;
|
||||
}
|
||||
|
||||
case JSONType_Object: {
|
||||
ret_string = indentDepth ? "{\n" + indentStr1 : "{";
|
||||
JSONObject::const_iterator iter = object_value->begin();
|
||||
while (iter != object_value->end()) {
|
||||
ret_string += StringifyString((*iter).first);
|
||||
ret_string += ":";
|
||||
ret_string += (*iter).second->StringifyImpl(indentDepth1);
|
||||
|
||||
// Not at the end - add a separator
|
||||
if (++iter != object_value->end())
|
||||
ret_string += ",";
|
||||
}
|
||||
ret_string += indentDepth ? "\n" + indentStr + "}" : "}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ret_string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a JSON encoded string with all required fields escaped
|
||||
* Works from http://www.ecma-internationl.org/publications/files/ECMA-ST/ECMA-262.pdf
|
||||
* Section 15.12.3.
|
||||
*
|
||||
* @access private
|
||||
*
|
||||
* @param std::string str The string that needs to have the characters escaped
|
||||
*
|
||||
* @return std::string Returns the JSON string
|
||||
*/
|
||||
std::string JSONValue::StringifyString(const std::string &str)
|
||||
{
|
||||
std::string str_out = "\"";
|
||||
|
||||
std::string::const_iterator iter = str.begin();
|
||||
while (iter != str.end()) {
|
||||
char chr = *iter;
|
||||
|
||||
if (chr == '"' || chr == '\\' || chr == '/') {
|
||||
str_out += '\\';
|
||||
str_out += chr;
|
||||
} else if (chr == '\b') {
|
||||
str_out += "\\b";
|
||||
} else if (chr == '\f') {
|
||||
str_out += "\\f";
|
||||
} else if (chr == '\n') {
|
||||
str_out += "\\n";
|
||||
} else if (chr == '\r') {
|
||||
str_out += "\\r";
|
||||
} else if (chr == '\t') {
|
||||
str_out += "\\t";
|
||||
} else if (chr < 0x20 || chr == 0x7F) {
|
||||
char buf[7];
|
||||
snprintf(buf, sizeof(buf), "\\u%04x", chr);
|
||||
str_out += buf;
|
||||
} else if (chr < 0x80) {
|
||||
str_out += chr;
|
||||
} else {
|
||||
str_out += chr;
|
||||
size_t remain = str.end() - iter - 1;
|
||||
if ((chr & 0xE0) == 0xC0 && remain >= 1) {
|
||||
++iter;
|
||||
str_out += *iter;
|
||||
} else if ((chr & 0xF0) == 0xE0 && remain >= 2) {
|
||||
str_out += *(++iter);
|
||||
str_out += *(++iter);
|
||||
} else if ((chr & 0xF8) == 0xF0 && remain >= 3) {
|
||||
str_out += *(++iter);
|
||||
str_out += *(++iter);
|
||||
str_out += *(++iter);
|
||||
}
|
||||
}
|
||||
|
||||
++iter;
|
||||
}
|
||||
|
||||
str_out += "\"";
|
||||
return str_out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the indentation string for the depth given
|
||||
*
|
||||
* @access private
|
||||
*
|
||||
* @param size_t indent The prettyprint indentation depth (0 : no indentation)
|
||||
*
|
||||
* @return std::string Returns the string
|
||||
*/
|
||||
std::string JSONValue::Indent(size_t depth)
|
||||
{
|
||||
const size_t indent_step = 2;
|
||||
depth ? --depth : 0;
|
||||
std::string indentStr(depth * indent_step, ' ');
|
||||
return indentStr;
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
/*
|
||||
* File JSONValue.h part of the SimpleJSON Library - http://mjpa.in/json
|
||||
*
|
||||
* Copyright (C) 2010 Mike Anchor
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef _JSONVALUE_H_
|
||||
#define _JSONVALUE_H_
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "JSON.h"
|
||||
|
||||
class JSON;
|
||||
|
||||
enum JSONType { JSONType_Null, JSONType_String, JSONType_Bool, JSONType_Number, JSONType_Array, JSONType_Object };
|
||||
|
||||
class JSONValue
|
||||
{
|
||||
friend class JSON;
|
||||
|
||||
public:
|
||||
JSONValue(/*NULL*/);
|
||||
explicit JSONValue(const char *m_char_value);
|
||||
explicit JSONValue(const std::string &m_string_value);
|
||||
explicit JSONValue(bool m_bool_value);
|
||||
explicit JSONValue(double m_number_value);
|
||||
explicit JSONValue(int m_integer_value);
|
||||
explicit JSONValue(unsigned int m_integer_value);
|
||||
explicit JSONValue(const JSONArray &m_array_value);
|
||||
explicit JSONValue(const JSONObject &m_object_value);
|
||||
explicit JSONValue(const JSONValue &m_source);
|
||||
~JSONValue();
|
||||
|
||||
bool IsNull() const;
|
||||
bool IsString() const;
|
||||
bool IsBool() const;
|
||||
bool IsNumber() const;
|
||||
bool IsArray() const;
|
||||
bool IsObject() const;
|
||||
|
||||
const std::string &AsString() const;
|
||||
bool AsBool() const;
|
||||
double AsNumber() const;
|
||||
const JSONArray &AsArray() const;
|
||||
const JSONObject &AsObject() const;
|
||||
|
||||
std::size_t CountChildren() const;
|
||||
bool HasChild(std::size_t index) const;
|
||||
JSONValue *Child(std::size_t index);
|
||||
bool HasChild(const char *name) const;
|
||||
JSONValue *Child(const char *name);
|
||||
std::vector<std::string> ObjectKeys() const;
|
||||
|
||||
std::string Stringify(bool const prettyprint = false) const;
|
||||
|
||||
protected:
|
||||
static JSONValue *Parse(const char **data);
|
||||
|
||||
private:
|
||||
static std::string StringifyString(const std::string &str);
|
||||
std::string StringifyImpl(size_t const indentDepth) const;
|
||||
static std::string Indent(size_t depth);
|
||||
|
||||
JSONType type;
|
||||
|
||||
union {
|
||||
bool bool_value;
|
||||
double number_value;
|
||||
std::string *string_value;
|
||||
JSONArray *array_value;
|
||||
JSONObject *object_value;
|
||||
};
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -1,11 +1,12 @@
|
||||
#ifndef NRF52_USE_JSON
|
||||
#if ARCH_PORTDUINO
|
||||
#include "MeshPacketSerializer.h"
|
||||
#include "JSON.h"
|
||||
#include "NodeDB.h"
|
||||
#include "mesh/generated/meshtastic/mqtt.pb.h"
|
||||
#include "mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "modules/RoutingModule.h"
|
||||
#include <DebugConfiguration.h>
|
||||
#include <json/json.h>
|
||||
#include <memory>
|
||||
#include <mesh-pb-constants.h>
|
||||
#if defined(ARCH_ESP32)
|
||||
#include "../mesh/generated/meshtastic/paxcount.pb.h"
|
||||
@@ -15,41 +16,49 @@
|
||||
|
||||
static const char *errStr = "Error decoding proto for %s message!";
|
||||
|
||||
static std::string writeCompact(const Json::Value &v)
|
||||
{
|
||||
Json::StreamWriterBuilder b;
|
||||
b["indentation"] = "";
|
||||
b["emitUTF8"] = true;
|
||||
return Json::writeString(b, v);
|
||||
}
|
||||
|
||||
static bool tryParseJson(const char *s, Json::Value &out)
|
||||
{
|
||||
Json::CharReaderBuilder b;
|
||||
std::unique_ptr<Json::CharReader> reader(b.newCharReader());
|
||||
std::string errs;
|
||||
const char *end = s + strlen(s);
|
||||
return reader->parse(s, end, &out, &errs);
|
||||
}
|
||||
|
||||
std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, bool shouldLog)
|
||||
{
|
||||
// the created jsonObj is immutable after creation, so
|
||||
// we need to do the heavy lifting before assembling it.
|
||||
std::string msgType;
|
||||
JSONObject jsonObj;
|
||||
Json::Value jsonObj(Json::objectValue);
|
||||
|
||||
if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
|
||||
JSONObject msgPayload;
|
||||
Json::Value msgPayload(Json::objectValue);
|
||||
switch (mp->decoded.portnum) {
|
||||
case meshtastic_PortNum_TEXT_MESSAGE_APP: {
|
||||
msgType = "text";
|
||||
// convert bytes to string
|
||||
if (shouldLog)
|
||||
LOG_DEBUG("got text message of size %u", mp->decoded.payload.size);
|
||||
|
||||
char payloadStr[(mp->decoded.payload.size) + 1];
|
||||
memcpy(payloadStr, mp->decoded.payload.bytes, mp->decoded.payload.size);
|
||||
payloadStr[mp->decoded.payload.size] = 0; // null terminated string
|
||||
// check if this is a JSON payload
|
||||
JSONValue *json_value = JSON::Parse(payloadStr);
|
||||
if (json_value != NULL) {
|
||||
payloadStr[mp->decoded.payload.size] = 0;
|
||||
Json::Value parsed;
|
||||
if (tryParseJson(payloadStr, parsed)) {
|
||||
if (shouldLog)
|
||||
LOG_INFO("text message payload is of type json");
|
||||
|
||||
// if it is, then we can just use the json object
|
||||
jsonObj["payload"] = json_value;
|
||||
jsonObj["payload"] = parsed;
|
||||
} else {
|
||||
// if it isn't, then we need to create a json object
|
||||
// with the string as the value
|
||||
if (shouldLog)
|
||||
LOG_INFO("text message payload is of type plaintext");
|
||||
|
||||
msgPayload["text"] = new JSONValue(payloadStr);
|
||||
jsonObj["payload"] = new JSONValue(msgPayload);
|
||||
msgPayload["text"] = payloadStr;
|
||||
jsonObj["payload"] = msgPayload;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -61,133 +70,129 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
|
||||
if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Telemetry_msg, &scratch)) {
|
||||
decoded = &scratch;
|
||||
if (decoded->which_variant == meshtastic_Telemetry_device_metrics_tag) {
|
||||
// If battery is present, encode the battery level value
|
||||
// TODO - Add a condition to send a code for a non-present value
|
||||
if (decoded->variant.device_metrics.has_battery_level) {
|
||||
msgPayload["battery_level"] = new JSONValue((int)decoded->variant.device_metrics.battery_level);
|
||||
msgPayload["battery_level"] = (int)decoded->variant.device_metrics.battery_level;
|
||||
}
|
||||
msgPayload["voltage"] = new JSONValue(decoded->variant.device_metrics.voltage);
|
||||
msgPayload["channel_utilization"] = new JSONValue(decoded->variant.device_metrics.channel_utilization);
|
||||
msgPayload["air_util_tx"] = new JSONValue(decoded->variant.device_metrics.air_util_tx);
|
||||
msgPayload["uptime_seconds"] = new JSONValue((unsigned int)decoded->variant.device_metrics.uptime_seconds);
|
||||
msgPayload["voltage"] = decoded->variant.device_metrics.voltage;
|
||||
msgPayload["channel_utilization"] = decoded->variant.device_metrics.channel_utilization;
|
||||
msgPayload["air_util_tx"] = decoded->variant.device_metrics.air_util_tx;
|
||||
msgPayload["uptime_seconds"] = (Json::UInt)decoded->variant.device_metrics.uptime_seconds;
|
||||
} else if (decoded->which_variant == meshtastic_Telemetry_environment_metrics_tag) {
|
||||
// Avoid sending 0s for sensors that could be 0
|
||||
if (decoded->variant.environment_metrics.has_temperature) {
|
||||
msgPayload["temperature"] = new JSONValue(decoded->variant.environment_metrics.temperature);
|
||||
msgPayload["temperature"] = decoded->variant.environment_metrics.temperature;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_relative_humidity) {
|
||||
msgPayload["relative_humidity"] = new JSONValue(decoded->variant.environment_metrics.relative_humidity);
|
||||
msgPayload["relative_humidity"] = decoded->variant.environment_metrics.relative_humidity;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_barometric_pressure) {
|
||||
msgPayload["barometric_pressure"] =
|
||||
new JSONValue(decoded->variant.environment_metrics.barometric_pressure);
|
||||
msgPayload["barometric_pressure"] = decoded->variant.environment_metrics.barometric_pressure;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_gas_resistance) {
|
||||
msgPayload["gas_resistance"] = new JSONValue(decoded->variant.environment_metrics.gas_resistance);
|
||||
msgPayload["gas_resistance"] = decoded->variant.environment_metrics.gas_resistance;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_voltage) {
|
||||
msgPayload["voltage"] = new JSONValue(decoded->variant.environment_metrics.voltage);
|
||||
msgPayload["voltage"] = decoded->variant.environment_metrics.voltage;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_current) {
|
||||
msgPayload["current"] = new JSONValue(decoded->variant.environment_metrics.current);
|
||||
msgPayload["current"] = decoded->variant.environment_metrics.current;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_lux) {
|
||||
msgPayload["lux"] = new JSONValue(decoded->variant.environment_metrics.lux);
|
||||
msgPayload["lux"] = decoded->variant.environment_metrics.lux;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_white_lux) {
|
||||
msgPayload["white_lux"] = new JSONValue(decoded->variant.environment_metrics.white_lux);
|
||||
msgPayload["white_lux"] = decoded->variant.environment_metrics.white_lux;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_iaq) {
|
||||
msgPayload["iaq"] = new JSONValue((uint)decoded->variant.environment_metrics.iaq);
|
||||
msgPayload["iaq"] = (Json::UInt)decoded->variant.environment_metrics.iaq;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_distance) {
|
||||
msgPayload["distance"] = new JSONValue(decoded->variant.environment_metrics.distance);
|
||||
msgPayload["distance"] = decoded->variant.environment_metrics.distance;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_wind_speed) {
|
||||
msgPayload["wind_speed"] = new JSONValue(decoded->variant.environment_metrics.wind_speed);
|
||||
msgPayload["wind_speed"] = decoded->variant.environment_metrics.wind_speed;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_wind_direction) {
|
||||
msgPayload["wind_direction"] = new JSONValue((uint)decoded->variant.environment_metrics.wind_direction);
|
||||
msgPayload["wind_direction"] = (Json::UInt)decoded->variant.environment_metrics.wind_direction;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_wind_gust) {
|
||||
msgPayload["wind_gust"] = new JSONValue(decoded->variant.environment_metrics.wind_gust);
|
||||
msgPayload["wind_gust"] = decoded->variant.environment_metrics.wind_gust;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_wind_lull) {
|
||||
msgPayload["wind_lull"] = new JSONValue(decoded->variant.environment_metrics.wind_lull);
|
||||
msgPayload["wind_lull"] = decoded->variant.environment_metrics.wind_lull;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_radiation) {
|
||||
msgPayload["radiation"] = new JSONValue(decoded->variant.environment_metrics.radiation);
|
||||
msgPayload["radiation"] = decoded->variant.environment_metrics.radiation;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_ir_lux) {
|
||||
msgPayload["ir_lux"] = new JSONValue(decoded->variant.environment_metrics.ir_lux);
|
||||
msgPayload["ir_lux"] = decoded->variant.environment_metrics.ir_lux;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_uv_lux) {
|
||||
msgPayload["uv_lux"] = new JSONValue(decoded->variant.environment_metrics.uv_lux);
|
||||
msgPayload["uv_lux"] = decoded->variant.environment_metrics.uv_lux;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_weight) {
|
||||
msgPayload["weight"] = new JSONValue(decoded->variant.environment_metrics.weight);
|
||||
msgPayload["weight"] = decoded->variant.environment_metrics.weight;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_rainfall_1h) {
|
||||
msgPayload["rainfall_1h"] = new JSONValue(decoded->variant.environment_metrics.rainfall_1h);
|
||||
msgPayload["rainfall_1h"] = decoded->variant.environment_metrics.rainfall_1h;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_rainfall_24h) {
|
||||
msgPayload["rainfall_24h"] = new JSONValue(decoded->variant.environment_metrics.rainfall_24h);
|
||||
msgPayload["rainfall_24h"] = decoded->variant.environment_metrics.rainfall_24h;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_soil_moisture) {
|
||||
msgPayload["soil_moisture"] = new JSONValue((uint)decoded->variant.environment_metrics.soil_moisture);
|
||||
msgPayload["soil_moisture"] = (Json::UInt)decoded->variant.environment_metrics.soil_moisture;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_soil_temperature) {
|
||||
msgPayload["soil_temperature"] = new JSONValue(decoded->variant.environment_metrics.soil_temperature);
|
||||
msgPayload["soil_temperature"] = decoded->variant.environment_metrics.soil_temperature;
|
||||
}
|
||||
} else if (decoded->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) {
|
||||
if (decoded->variant.air_quality_metrics.has_pm10_standard) {
|
||||
msgPayload["pm10"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_standard);
|
||||
msgPayload["pm10"] = (Json::UInt)decoded->variant.air_quality_metrics.pm10_standard;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_pm25_standard) {
|
||||
msgPayload["pm25"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_standard);
|
||||
msgPayload["pm25"] = (Json::UInt)decoded->variant.air_quality_metrics.pm25_standard;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_pm100_standard) {
|
||||
msgPayload["pm100"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_standard);
|
||||
msgPayload["pm100"] = (Json::UInt)decoded->variant.air_quality_metrics.pm100_standard;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_co2) {
|
||||
msgPayload["co2"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.co2);
|
||||
msgPayload["co2"] = (Json::UInt)decoded->variant.air_quality_metrics.co2;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_co2_temperature) {
|
||||
msgPayload["co2_temperature"] = new JSONValue(decoded->variant.air_quality_metrics.co2_temperature);
|
||||
msgPayload["co2_temperature"] = decoded->variant.air_quality_metrics.co2_temperature;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_co2_humidity) {
|
||||
msgPayload["co2_humidity"] = new JSONValue(decoded->variant.air_quality_metrics.co2_humidity);
|
||||
msgPayload["co2_humidity"] = decoded->variant.air_quality_metrics.co2_humidity;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_form_formaldehyde) {
|
||||
msgPayload["form_formaldehyde"] = new JSONValue(decoded->variant.air_quality_metrics.form_formaldehyde);
|
||||
msgPayload["form_formaldehyde"] = decoded->variant.air_quality_metrics.form_formaldehyde;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_form_temperature) {
|
||||
msgPayload["form_temperature"] = new JSONValue(decoded->variant.air_quality_metrics.form_temperature);
|
||||
msgPayload["form_temperature"] = decoded->variant.air_quality_metrics.form_temperature;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_form_humidity) {
|
||||
msgPayload["form_humidity"] = new JSONValue(decoded->variant.air_quality_metrics.form_humidity);
|
||||
msgPayload["form_humidity"] = decoded->variant.air_quality_metrics.form_humidity;
|
||||
}
|
||||
} else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) {
|
||||
if (decoded->variant.power_metrics.has_ch1_voltage) {
|
||||
msgPayload["voltage_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_voltage);
|
||||
msgPayload["voltage_ch1"] = decoded->variant.power_metrics.ch1_voltage;
|
||||
}
|
||||
if (decoded->variant.power_metrics.has_ch1_current) {
|
||||
msgPayload["current_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_current);
|
||||
msgPayload["current_ch1"] = decoded->variant.power_metrics.ch1_current;
|
||||
}
|
||||
if (decoded->variant.power_metrics.has_ch2_voltage) {
|
||||
msgPayload["voltage_ch2"] = new JSONValue(decoded->variant.power_metrics.ch2_voltage);
|
||||
msgPayload["voltage_ch2"] = decoded->variant.power_metrics.ch2_voltage;
|
||||
}
|
||||
if (decoded->variant.power_metrics.has_ch2_current) {
|
||||
msgPayload["current_ch2"] = new JSONValue(decoded->variant.power_metrics.ch2_current);
|
||||
msgPayload["current_ch2"] = decoded->variant.power_metrics.ch2_current;
|
||||
}
|
||||
if (decoded->variant.power_metrics.has_ch3_voltage) {
|
||||
msgPayload["voltage_ch3"] = new JSONValue(decoded->variant.power_metrics.ch3_voltage);
|
||||
msgPayload["voltage_ch3"] = decoded->variant.power_metrics.ch3_voltage;
|
||||
}
|
||||
if (decoded->variant.power_metrics.has_ch3_current) {
|
||||
msgPayload["current_ch3"] = new JSONValue(decoded->variant.power_metrics.ch3_current);
|
||||
msgPayload["current_ch3"] = decoded->variant.power_metrics.ch3_current;
|
||||
}
|
||||
}
|
||||
jsonObj["payload"] = new JSONValue(msgPayload);
|
||||
jsonObj["payload"] = msgPayload;
|
||||
} else if (shouldLog) {
|
||||
LOG_ERROR(errStr, msgType.c_str());
|
||||
}
|
||||
@@ -200,12 +205,12 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
|
||||
memset(&scratch, 0, sizeof(scratch));
|
||||
if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_User_msg, &scratch)) {
|
||||
decoded = &scratch;
|
||||
msgPayload["id"] = new JSONValue(decoded->id);
|
||||
msgPayload["longname"] = new JSONValue(decoded->long_name);
|
||||
msgPayload["shortname"] = new JSONValue(decoded->short_name);
|
||||
msgPayload["hardware"] = new JSONValue(decoded->hw_model);
|
||||
msgPayload["role"] = new JSONValue((int)decoded->role);
|
||||
jsonObj["payload"] = new JSONValue(msgPayload);
|
||||
msgPayload["id"] = decoded->id;
|
||||
msgPayload["longname"] = decoded->long_name;
|
||||
msgPayload["shortname"] = decoded->short_name;
|
||||
msgPayload["hardware"] = (int)decoded->hw_model;
|
||||
msgPayload["role"] = (int)decoded->role;
|
||||
jsonObj["payload"] = msgPayload;
|
||||
} else if (shouldLog) {
|
||||
LOG_ERROR(errStr, msgType.c_str());
|
||||
}
|
||||
@@ -219,38 +224,38 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
|
||||
if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Position_msg, &scratch)) {
|
||||
decoded = &scratch;
|
||||
if ((int)decoded->time) {
|
||||
msgPayload["time"] = new JSONValue((unsigned int)decoded->time);
|
||||
msgPayload["time"] = (Json::UInt)decoded->time;
|
||||
}
|
||||
if ((int)decoded->timestamp) {
|
||||
msgPayload["timestamp"] = new JSONValue((unsigned int)decoded->timestamp);
|
||||
msgPayload["timestamp"] = (Json::UInt)decoded->timestamp;
|
||||
}
|
||||
msgPayload["latitude_i"] = new JSONValue((int)decoded->latitude_i);
|
||||
msgPayload["longitude_i"] = new JSONValue((int)decoded->longitude_i);
|
||||
msgPayload["latitude_i"] = (int)decoded->latitude_i;
|
||||
msgPayload["longitude_i"] = (int)decoded->longitude_i;
|
||||
if ((int)decoded->altitude) {
|
||||
msgPayload["altitude"] = new JSONValue((int)decoded->altitude);
|
||||
msgPayload["altitude"] = (int)decoded->altitude;
|
||||
}
|
||||
if ((int)decoded->ground_speed) {
|
||||
msgPayload["ground_speed"] = new JSONValue((unsigned int)decoded->ground_speed);
|
||||
msgPayload["ground_speed"] = (Json::UInt)decoded->ground_speed;
|
||||
}
|
||||
if (int(decoded->ground_track)) {
|
||||
msgPayload["ground_track"] = new JSONValue((unsigned int)decoded->ground_track);
|
||||
msgPayload["ground_track"] = (Json::UInt)decoded->ground_track;
|
||||
}
|
||||
if (int(decoded->sats_in_view)) {
|
||||
msgPayload["sats_in_view"] = new JSONValue((unsigned int)decoded->sats_in_view);
|
||||
msgPayload["sats_in_view"] = (Json::UInt)decoded->sats_in_view;
|
||||
}
|
||||
if ((int)decoded->PDOP) {
|
||||
msgPayload["PDOP"] = new JSONValue((int)decoded->PDOP);
|
||||
msgPayload["PDOP"] = (int)decoded->PDOP;
|
||||
}
|
||||
if ((int)decoded->HDOP) {
|
||||
msgPayload["HDOP"] = new JSONValue((int)decoded->HDOP);
|
||||
msgPayload["HDOP"] = (int)decoded->HDOP;
|
||||
}
|
||||
if ((int)decoded->VDOP) {
|
||||
msgPayload["VDOP"] = new JSONValue((int)decoded->VDOP);
|
||||
msgPayload["VDOP"] = (int)decoded->VDOP;
|
||||
}
|
||||
if ((int)decoded->precision_bits) {
|
||||
msgPayload["precision_bits"] = new JSONValue((int)decoded->precision_bits);
|
||||
msgPayload["precision_bits"] = (int)decoded->precision_bits;
|
||||
}
|
||||
jsonObj["payload"] = new JSONValue(msgPayload);
|
||||
jsonObj["payload"] = msgPayload;
|
||||
} else if (shouldLog) {
|
||||
LOG_ERROR(errStr, msgType.c_str());
|
||||
}
|
||||
@@ -263,14 +268,14 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
|
||||
memset(&scratch, 0, sizeof(scratch));
|
||||
if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Waypoint_msg, &scratch)) {
|
||||
decoded = &scratch;
|
||||
msgPayload["id"] = new JSONValue((unsigned int)decoded->id);
|
||||
msgPayload["name"] = new JSONValue(decoded->name);
|
||||
msgPayload["description"] = new JSONValue(decoded->description);
|
||||
msgPayload["expire"] = new JSONValue((unsigned int)decoded->expire);
|
||||
msgPayload["locked_to"] = new JSONValue((unsigned int)decoded->locked_to);
|
||||
msgPayload["latitude_i"] = new JSONValue((int)decoded->latitude_i);
|
||||
msgPayload["longitude_i"] = new JSONValue((int)decoded->longitude_i);
|
||||
jsonObj["payload"] = new JSONValue(msgPayload);
|
||||
msgPayload["id"] = (Json::UInt)decoded->id;
|
||||
msgPayload["name"] = decoded->name;
|
||||
msgPayload["description"] = decoded->description;
|
||||
msgPayload["expire"] = (Json::UInt)decoded->expire;
|
||||
msgPayload["locked_to"] = (Json::UInt)decoded->locked_to;
|
||||
msgPayload["latitude_i"] = (int)decoded->latitude_i;
|
||||
msgPayload["longitude_i"] = (int)decoded->longitude_i;
|
||||
jsonObj["payload"] = msgPayload;
|
||||
} else if (shouldLog) {
|
||||
LOG_ERROR(errStr, msgType.c_str());
|
||||
}
|
||||
@@ -284,26 +289,26 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
|
||||
if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_NeighborInfo_msg,
|
||||
&scratch)) {
|
||||
decoded = &scratch;
|
||||
msgPayload["node_id"] = new JSONValue((unsigned int)decoded->node_id);
|
||||
msgPayload["node_broadcast_interval_secs"] = new JSONValue((unsigned int)decoded->node_broadcast_interval_secs);
|
||||
msgPayload["last_sent_by_id"] = new JSONValue((unsigned int)decoded->last_sent_by_id);
|
||||
msgPayload["neighbors_count"] = new JSONValue(decoded->neighbors_count);
|
||||
JSONArray neighbors;
|
||||
msgPayload["node_id"] = (Json::UInt)decoded->node_id;
|
||||
msgPayload["node_broadcast_interval_secs"] = (Json::UInt)decoded->node_broadcast_interval_secs;
|
||||
msgPayload["last_sent_by_id"] = (Json::UInt)decoded->last_sent_by_id;
|
||||
msgPayload["neighbors_count"] = (Json::UInt)decoded->neighbors_count;
|
||||
Json::Value neighbors(Json::arrayValue);
|
||||
for (uint8_t i = 0; i < decoded->neighbors_count; i++) {
|
||||
JSONObject neighborObj;
|
||||
neighborObj["node_id"] = new JSONValue((unsigned int)decoded->neighbors[i].node_id);
|
||||
neighborObj["snr"] = new JSONValue((int)decoded->neighbors[i].snr);
|
||||
neighbors.push_back(new JSONValue(neighborObj));
|
||||
Json::Value neighborObj(Json::objectValue);
|
||||
neighborObj["node_id"] = (Json::UInt)decoded->neighbors[i].node_id;
|
||||
neighborObj["snr"] = (int)decoded->neighbors[i].snr;
|
||||
neighbors.append(neighborObj);
|
||||
}
|
||||
msgPayload["neighbors"] = new JSONValue(neighbors);
|
||||
jsonObj["payload"] = new JSONValue(msgPayload);
|
||||
msgPayload["neighbors"] = neighbors;
|
||||
jsonObj["payload"] = msgPayload;
|
||||
} else if (shouldLog) {
|
||||
LOG_ERROR(errStr, msgType.c_str());
|
||||
}
|
||||
break;
|
||||
}
|
||||
case meshtastic_PortNum_TRACEROUTE_APP: {
|
||||
if (mp->decoded.request_id) { // Only report the traceroute response
|
||||
if (mp->decoded.request_id) {
|
||||
msgType = "traceroute";
|
||||
meshtastic_RouteDiscovery scratch;
|
||||
meshtastic_RouteDiscovery *decoded = NULL;
|
||||
@@ -311,45 +316,43 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
|
||||
if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_RouteDiscovery_msg,
|
||||
&scratch)) {
|
||||
decoded = &scratch;
|
||||
JSONArray route; // Route this message took
|
||||
JSONArray routeBack; // Route this message took back
|
||||
JSONArray snrTowards; // Snr for forward route
|
||||
JSONArray snrBack; // Snr for reverse route
|
||||
Json::Value route(Json::arrayValue);
|
||||
Json::Value routeBack(Json::arrayValue);
|
||||
Json::Value snrTowards(Json::arrayValue);
|
||||
Json::Value snrBack(Json::arrayValue);
|
||||
|
||||
// Lambda function for adding a long name to the route
|
||||
auto addToRoute = [](JSONArray *route, NodeNum num) {
|
||||
auto addToRoute = [](Json::Value *r, NodeNum num) {
|
||||
char long_name[40] = "Unknown";
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num);
|
||||
bool name_known = node ? node->has_user : false;
|
||||
if (name_known)
|
||||
memcpy(long_name, node->user.long_name, sizeof(long_name));
|
||||
route->push_back(new JSONValue(long_name));
|
||||
r->append(long_name);
|
||||
};
|
||||
addToRoute(&route, mp->to); // Started at the original transmitter (destination of response)
|
||||
addToRoute(&route, mp->to);
|
||||
for (uint8_t i = 0; i < decoded->route_count; i++) {
|
||||
addToRoute(&route, decoded->route[i]);
|
||||
}
|
||||
addToRoute(&route, mp->from); // Ended at the original destination (source of response)
|
||||
addToRoute(&route, mp->from);
|
||||
|
||||
addToRoute(&routeBack, mp->from); // Started at the original destination (source of response)
|
||||
addToRoute(&routeBack, mp->from);
|
||||
for (uint8_t i = 0; i < decoded->route_back_count; i++) {
|
||||
addToRoute(&routeBack, decoded->route_back[i]);
|
||||
}
|
||||
addToRoute(&routeBack, mp->to); // Ended at the original transmitter (destination of response)
|
||||
addToRoute(&routeBack, mp->to);
|
||||
|
||||
for (uint8_t i = 0; i < decoded->snr_back_count; i++) {
|
||||
snrBack.push_back(new JSONValue((float)decoded->snr_back[i] / 4));
|
||||
snrBack.append((float)decoded->snr_back[i] / 4);
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < decoded->snr_towards_count; i++) {
|
||||
snrTowards.push_back(new JSONValue((float)decoded->snr_towards[i] / 4));
|
||||
snrTowards.append((float)decoded->snr_towards[i] / 4);
|
||||
}
|
||||
|
||||
msgPayload["route"] = new JSONValue(route);
|
||||
msgPayload["route_back"] = new JSONValue(routeBack);
|
||||
msgPayload["snr_back"] = new JSONValue(snrBack);
|
||||
msgPayload["snr_towards"] = new JSONValue(snrTowards);
|
||||
jsonObj["payload"] = new JSONValue(msgPayload);
|
||||
msgPayload["route"] = route;
|
||||
msgPayload["route_back"] = routeBack;
|
||||
msgPayload["snr_back"] = snrBack;
|
||||
msgPayload["snr_towards"] = snrTowards;
|
||||
jsonObj["payload"] = msgPayload;
|
||||
} else if (shouldLog) {
|
||||
LOG_ERROR(errStr, msgType.c_str());
|
||||
}
|
||||
@@ -360,9 +363,9 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
|
||||
msgType = "detection";
|
||||
char payloadStr[(mp->decoded.payload.size) + 1];
|
||||
memcpy(payloadStr, mp->decoded.payload.bytes, mp->decoded.payload.size);
|
||||
payloadStr[mp->decoded.payload.size] = 0; // null terminated string
|
||||
msgPayload["text"] = new JSONValue(payloadStr);
|
||||
jsonObj["payload"] = new JSONValue(msgPayload);
|
||||
payloadStr[mp->decoded.payload.size] = 0;
|
||||
msgPayload["text"] = payloadStr;
|
||||
jsonObj["payload"] = msgPayload;
|
||||
break;
|
||||
}
|
||||
#ifdef ARCH_ESP32
|
||||
@@ -373,10 +376,10 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
|
||||
memset(&scratch, 0, sizeof(scratch));
|
||||
if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Paxcount_msg, &scratch)) {
|
||||
decoded = &scratch;
|
||||
msgPayload["wifi_count"] = new JSONValue((unsigned int)decoded->wifi);
|
||||
msgPayload["ble_count"] = new JSONValue((unsigned int)decoded->ble);
|
||||
msgPayload["uptime"] = new JSONValue((unsigned int)decoded->uptime);
|
||||
jsonObj["payload"] = new JSONValue(msgPayload);
|
||||
msgPayload["wifi_count"] = (Json::UInt)decoded->wifi;
|
||||
msgPayload["ble_count"] = (Json::UInt)decoded->ble;
|
||||
msgPayload["uptime"] = (Json::UInt)decoded->uptime;
|
||||
jsonObj["payload"] = msgPayload;
|
||||
} else if (shouldLog) {
|
||||
LOG_ERROR(errStr, msgType.c_str());
|
||||
}
|
||||
@@ -392,20 +395,19 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
|
||||
decoded = &scratch;
|
||||
if (decoded->type == meshtastic_HardwareMessage_Type_GPIOS_CHANGED) {
|
||||
msgType = "gpios_changed";
|
||||
msgPayload["gpio_value"] = new JSONValue((unsigned int)decoded->gpio_value);
|
||||
jsonObj["payload"] = new JSONValue(msgPayload);
|
||||
msgPayload["gpio_value"] = (Json::UInt)decoded->gpio_value;
|
||||
jsonObj["payload"] = msgPayload;
|
||||
} else if (decoded->type == meshtastic_HardwareMessage_Type_READ_GPIOS_REPLY) {
|
||||
msgType = "gpios_read_reply";
|
||||
msgPayload["gpio_value"] = new JSONValue((unsigned int)decoded->gpio_value);
|
||||
msgPayload["gpio_mask"] = new JSONValue((unsigned int)decoded->gpio_mask);
|
||||
jsonObj["payload"] = new JSONValue(msgPayload);
|
||||
msgPayload["gpio_value"] = (Json::UInt)decoded->gpio_value;
|
||||
msgPayload["gpio_mask"] = (Json::UInt)decoded->gpio_mask;
|
||||
jsonObj["payload"] = msgPayload;
|
||||
}
|
||||
} else if (shouldLog) {
|
||||
LOG_ERROR(errStr, "RemoteHardware");
|
||||
}
|
||||
break;
|
||||
}
|
||||
// add more packet types here if needed
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -413,64 +415,56 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
|
||||
LOG_WARN("Couldn't convert encrypted payload of MeshPacket to JSON");
|
||||
}
|
||||
|
||||
jsonObj["id"] = new JSONValue((unsigned int)mp->id);
|
||||
jsonObj["timestamp"] = new JSONValue((unsigned int)mp->rx_time);
|
||||
jsonObj["to"] = new JSONValue((unsigned int)mp->to);
|
||||
jsonObj["from"] = new JSONValue((unsigned int)mp->from);
|
||||
jsonObj["channel"] = new JSONValue((unsigned int)mp->channel);
|
||||
jsonObj["type"] = new JSONValue(msgType.c_str());
|
||||
jsonObj["sender"] = new JSONValue(nodeDB->getNodeId().c_str());
|
||||
jsonObj["id"] = (Json::UInt)mp->id;
|
||||
jsonObj["timestamp"] = (Json::UInt)mp->rx_time;
|
||||
jsonObj["to"] = (Json::UInt)mp->to;
|
||||
jsonObj["from"] = (Json::UInt)mp->from;
|
||||
jsonObj["channel"] = (Json::UInt)mp->channel;
|
||||
jsonObj["type"] = msgType;
|
||||
jsonObj["sender"] = nodeDB->getNodeId();
|
||||
if (mp->rx_rssi != 0)
|
||||
jsonObj["rssi"] = new JSONValue((int)mp->rx_rssi);
|
||||
jsonObj["rssi"] = (int)mp->rx_rssi;
|
||||
if (mp->rx_snr != 0)
|
||||
jsonObj["snr"] = new JSONValue((float)mp->rx_snr);
|
||||
jsonObj["snr"] = (float)mp->rx_snr;
|
||||
const int8_t hopsAway = getHopsAway(*mp);
|
||||
if (hopsAway >= 0) {
|
||||
jsonObj["hops_away"] = new JSONValue((unsigned int)(hopsAway));
|
||||
jsonObj["hop_start"] = new JSONValue((unsigned int)(mp->hop_start));
|
||||
jsonObj["hops_away"] = (Json::UInt)(hopsAway);
|
||||
jsonObj["hop_start"] = (Json::UInt)(mp->hop_start);
|
||||
}
|
||||
|
||||
// serialize and write it to the stream
|
||||
JSONValue *value = new JSONValue(jsonObj);
|
||||
std::string jsonStr = value->Stringify();
|
||||
std::string jsonStr = writeCompact(jsonObj);
|
||||
|
||||
if (shouldLog)
|
||||
LOG_INFO("serialized json message: %s", jsonStr.c_str());
|
||||
|
||||
delete value;
|
||||
return jsonStr;
|
||||
}
|
||||
|
||||
std::string MeshPacketSerializer::JsonSerializeEncrypted(const meshtastic_MeshPacket *mp)
|
||||
{
|
||||
JSONObject jsonObj;
|
||||
Json::Value jsonObj(Json::objectValue);
|
||||
|
||||
jsonObj["id"] = new JSONValue((unsigned int)mp->id);
|
||||
jsonObj["time_ms"] = new JSONValue((double)millis());
|
||||
jsonObj["timestamp"] = new JSONValue((unsigned int)mp->rx_time);
|
||||
jsonObj["to"] = new JSONValue((unsigned int)mp->to);
|
||||
jsonObj["from"] = new JSONValue((unsigned int)mp->from);
|
||||
jsonObj["channel"] = new JSONValue((unsigned int)mp->channel);
|
||||
jsonObj["want_ack"] = new JSONValue(mp->want_ack);
|
||||
jsonObj["id"] = (Json::UInt)mp->id;
|
||||
jsonObj["time_ms"] = (double)millis();
|
||||
jsonObj["timestamp"] = (Json::UInt)mp->rx_time;
|
||||
jsonObj["to"] = (Json::UInt)mp->to;
|
||||
jsonObj["from"] = (Json::UInt)mp->from;
|
||||
jsonObj["channel"] = (Json::UInt)mp->channel;
|
||||
jsonObj["want_ack"] = mp->want_ack;
|
||||
|
||||
if (mp->rx_rssi != 0)
|
||||
jsonObj["rssi"] = new JSONValue((int)mp->rx_rssi);
|
||||
jsonObj["rssi"] = (int)mp->rx_rssi;
|
||||
if (mp->rx_snr != 0)
|
||||
jsonObj["snr"] = new JSONValue((float)mp->rx_snr);
|
||||
jsonObj["snr"] = (float)mp->rx_snr;
|
||||
const int8_t hopsAway = getHopsAway(*mp);
|
||||
if (hopsAway >= 0) {
|
||||
jsonObj["hops_away"] = new JSONValue((unsigned int)(hopsAway));
|
||||
jsonObj["hop_start"] = new JSONValue((unsigned int)(mp->hop_start));
|
||||
jsonObj["hops_away"] = (Json::UInt)(hopsAway);
|
||||
jsonObj["hop_start"] = (Json::UInt)(mp->hop_start);
|
||||
}
|
||||
jsonObj["size"] = new JSONValue((unsigned int)mp->encrypted.size);
|
||||
jsonObj["size"] = (Json::UInt)mp->encrypted.size;
|
||||
auto encryptedStr = bytesToHex(mp->encrypted.bytes, mp->encrypted.size);
|
||||
jsonObj["bytes"] = new JSONValue(encryptedStr.c_str());
|
||||
jsonObj["bytes"] = encryptedStr;
|
||||
|
||||
// serialize and write it to the stream
|
||||
JSONValue *value = new JSONValue(jsonObj);
|
||||
std::string jsonStr = value->Stringify();
|
||||
|
||||
delete value;
|
||||
return jsonStr;
|
||||
return writeCompact(jsonObj);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -1,421 +0,0 @@
|
||||
#ifdef NRF52_USE_JSON
|
||||
#warning 'Using nRF52 Serializer'
|
||||
|
||||
#include "ArduinoJson.h"
|
||||
#include "MeshPacketSerializer.h"
|
||||
#include "NodeDB.h"
|
||||
#include "mesh/generated/meshtastic/mqtt.pb.h"
|
||||
#include "mesh/generated/meshtastic/remote_hardware.pb.h"
|
||||
#include "mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "modules/RoutingModule.h"
|
||||
#include <DebugConfiguration.h>
|
||||
#include <mesh-pb-constants.h>
|
||||
|
||||
StaticJsonDocument<1024> jsonObj;
|
||||
StaticJsonDocument<1024> arrayObj;
|
||||
|
||||
std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, bool shouldLog)
|
||||
{
|
||||
// the created jsonObj is immutable after creation, so
|
||||
// we need to do the heavy lifting before assembling it.
|
||||
std::string msgType;
|
||||
jsonObj.clear();
|
||||
arrayObj.clear();
|
||||
|
||||
if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
|
||||
switch (mp->decoded.portnum) {
|
||||
case meshtastic_PortNum_TEXT_MESSAGE_APP: {
|
||||
msgType = "text";
|
||||
// convert bytes to string
|
||||
if (shouldLog)
|
||||
LOG_DEBUG("got text message of size %u", mp->decoded.payload.size);
|
||||
|
||||
char payloadStr[(mp->decoded.payload.size) + 1];
|
||||
memcpy(payloadStr, mp->decoded.payload.bytes, mp->decoded.payload.size);
|
||||
payloadStr[mp->decoded.payload.size] = 0; // null terminated string
|
||||
// check if this is a JSON payload
|
||||
StaticJsonDocument<512> text_doc;
|
||||
DeserializationError error = deserializeJson(text_doc, payloadStr);
|
||||
if (error) {
|
||||
// if it isn't, then we need to create a json object
|
||||
// with the string as the value
|
||||
if (shouldLog)
|
||||
LOG_INFO("text message payload is of type plaintext");
|
||||
jsonObj["payload"]["text"] = payloadStr;
|
||||
} else {
|
||||
// if it is, then we can just use the json object
|
||||
if (shouldLog)
|
||||
LOG_INFO("text message payload is of type json");
|
||||
jsonObj["payload"] = text_doc;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case meshtastic_PortNum_TELEMETRY_APP: {
|
||||
msgType = "telemetry";
|
||||
meshtastic_Telemetry scratch;
|
||||
meshtastic_Telemetry *decoded = NULL;
|
||||
memset(&scratch, 0, sizeof(scratch));
|
||||
if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Telemetry_msg, &scratch)) {
|
||||
decoded = &scratch;
|
||||
if (decoded->which_variant == meshtastic_Telemetry_device_metrics_tag) {
|
||||
// If battery is present, encode the battery level value
|
||||
// TODO - Add a condition to send a code for a non-present value
|
||||
if (decoded->variant.device_metrics.has_battery_level) {
|
||||
jsonObj["payload"]["battery_level"] = (int)decoded->variant.device_metrics.battery_level;
|
||||
}
|
||||
jsonObj["payload"]["voltage"] = decoded->variant.device_metrics.voltage;
|
||||
jsonObj["payload"]["channel_utilization"] = decoded->variant.device_metrics.channel_utilization;
|
||||
jsonObj["payload"]["air_util_tx"] = decoded->variant.device_metrics.air_util_tx;
|
||||
jsonObj["payload"]["uptime_seconds"] = (unsigned int)decoded->variant.device_metrics.uptime_seconds;
|
||||
} else if (decoded->which_variant == meshtastic_Telemetry_environment_metrics_tag) {
|
||||
if (decoded->variant.environment_metrics.has_temperature) {
|
||||
jsonObj["payload"]["temperature"] = decoded->variant.environment_metrics.temperature;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_relative_humidity) {
|
||||
jsonObj["payload"]["relative_humidity"] = decoded->variant.environment_metrics.relative_humidity;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_barometric_pressure) {
|
||||
jsonObj["payload"]["barometric_pressure"] = decoded->variant.environment_metrics.barometric_pressure;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_gas_resistance) {
|
||||
jsonObj["payload"]["gas_resistance"] = decoded->variant.environment_metrics.gas_resistance;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_voltage) {
|
||||
jsonObj["payload"]["voltage"] = decoded->variant.environment_metrics.voltage;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_current) {
|
||||
jsonObj["payload"]["current"] = decoded->variant.environment_metrics.current;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_lux) {
|
||||
jsonObj["payload"]["lux"] = decoded->variant.environment_metrics.lux;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_white_lux) {
|
||||
jsonObj["payload"]["white_lux"] = decoded->variant.environment_metrics.white_lux;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_iaq) {
|
||||
jsonObj["payload"]["iaq"] = (uint)decoded->variant.environment_metrics.iaq;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_wind_speed) {
|
||||
jsonObj["payload"]["wind_speed"] = decoded->variant.environment_metrics.wind_speed;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_wind_direction) {
|
||||
jsonObj["payload"]["wind_direction"] = (uint)decoded->variant.environment_metrics.wind_direction;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_wind_gust) {
|
||||
jsonObj["payload"]["wind_gust"] = decoded->variant.environment_metrics.wind_gust;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_wind_lull) {
|
||||
jsonObj["payload"]["wind_lull"] = decoded->variant.environment_metrics.wind_lull;
|
||||
}
|
||||
if (decoded->variant.environment_metrics.has_radiation) {
|
||||
jsonObj["payload"]["radiation"] = decoded->variant.environment_metrics.radiation;
|
||||
}
|
||||
} else if (decoded->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) {
|
||||
if (decoded->variant.air_quality_metrics.has_pm10_standard) {
|
||||
jsonObj["payload"]["pm10"] = (unsigned int)decoded->variant.air_quality_metrics.pm10_standard;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_pm25_standard) {
|
||||
jsonObj["payload"]["pm25"] = (unsigned int)decoded->variant.air_quality_metrics.pm25_standard;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_pm100_standard) {
|
||||
jsonObj["payload"]["pm100"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_standard;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_co2) {
|
||||
jsonObj["payload"]["co2"] = (unsigned int)decoded->variant.air_quality_metrics.co2;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_co2_temperature) {
|
||||
jsonObj["payload"]["co2_temperature"] = decoded->variant.air_quality_metrics.co2_temperature;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_co2_humidity) {
|
||||
jsonObj["payload"]["co2_humidity"] = decoded->variant.air_quality_metrics.co2_humidity;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_form_formaldehyde) {
|
||||
jsonObj["payload"]["form_formaldehyde"] = decoded->variant.air_quality_metrics.form_formaldehyde;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_form_temperature) {
|
||||
jsonObj["payload"]["form_temperature"] = decoded->variant.air_quality_metrics.form_temperature;
|
||||
}
|
||||
if (decoded->variant.air_quality_metrics.has_form_humidity) {
|
||||
jsonObj["payload"]["form_humidity"] = decoded->variant.air_quality_metrics.form_humidity;
|
||||
}
|
||||
} else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) {
|
||||
if (decoded->variant.power_metrics.has_ch1_voltage) {
|
||||
jsonObj["payload"]["voltage_ch1"] = decoded->variant.power_metrics.ch1_voltage;
|
||||
}
|
||||
if (decoded->variant.power_metrics.has_ch1_current) {
|
||||
jsonObj["payload"]["current_ch1"] = decoded->variant.power_metrics.ch1_current;
|
||||
}
|
||||
if (decoded->variant.power_metrics.has_ch2_voltage) {
|
||||
jsonObj["payload"]["voltage_ch2"] = decoded->variant.power_metrics.ch2_voltage;
|
||||
}
|
||||
if (decoded->variant.power_metrics.has_ch2_current) {
|
||||
jsonObj["payload"]["current_ch2"] = decoded->variant.power_metrics.ch2_current;
|
||||
}
|
||||
if (decoded->variant.power_metrics.has_ch3_voltage) {
|
||||
jsonObj["payload"]["voltage_ch3"] = decoded->variant.power_metrics.ch3_voltage;
|
||||
}
|
||||
if (decoded->variant.power_metrics.has_ch3_current) {
|
||||
jsonObj["payload"]["current_ch3"] = decoded->variant.power_metrics.ch3_current;
|
||||
}
|
||||
}
|
||||
} else if (shouldLog) {
|
||||
LOG_ERROR("Error decoding proto for telemetry message!");
|
||||
return "";
|
||||
}
|
||||
break;
|
||||
}
|
||||
case meshtastic_PortNum_NODEINFO_APP: {
|
||||
msgType = "nodeinfo";
|
||||
meshtastic_User scratch;
|
||||
meshtastic_User *decoded = NULL;
|
||||
memset(&scratch, 0, sizeof(scratch));
|
||||
if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_User_msg, &scratch)) {
|
||||
decoded = &scratch;
|
||||
jsonObj["payload"]["id"] = decoded->id;
|
||||
jsonObj["payload"]["longname"] = decoded->long_name;
|
||||
jsonObj["payload"]["shortname"] = decoded->short_name;
|
||||
jsonObj["payload"]["hardware"] = decoded->hw_model;
|
||||
jsonObj["payload"]["role"] = (int)decoded->role;
|
||||
} else if (shouldLog) {
|
||||
LOG_ERROR("Error decoding proto for nodeinfo message!");
|
||||
return "";
|
||||
}
|
||||
break;
|
||||
}
|
||||
case meshtastic_PortNum_POSITION_APP: {
|
||||
msgType = "position";
|
||||
meshtastic_Position scratch;
|
||||
meshtastic_Position *decoded = NULL;
|
||||
memset(&scratch, 0, sizeof(scratch));
|
||||
if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Position_msg, &scratch)) {
|
||||
decoded = &scratch;
|
||||
if ((int)decoded->time) {
|
||||
jsonObj["payload"]["time"] = (unsigned int)decoded->time;
|
||||
}
|
||||
if ((int)decoded->timestamp) {
|
||||
jsonObj["payload"]["timestamp"] = (unsigned int)decoded->timestamp;
|
||||
}
|
||||
jsonObj["payload"]["latitude_i"] = (int)decoded->latitude_i;
|
||||
jsonObj["payload"]["longitude_i"] = (int)decoded->longitude_i;
|
||||
if ((int)decoded->altitude) {
|
||||
jsonObj["payload"]["altitude"] = (int)decoded->altitude;
|
||||
}
|
||||
if ((int)decoded->ground_speed) {
|
||||
jsonObj["payload"]["ground_speed"] = (unsigned int)decoded->ground_speed;
|
||||
}
|
||||
if (int(decoded->ground_track)) {
|
||||
jsonObj["payload"]["ground_track"] = (unsigned int)decoded->ground_track;
|
||||
}
|
||||
if (int(decoded->sats_in_view)) {
|
||||
jsonObj["payload"]["sats_in_view"] = (unsigned int)decoded->sats_in_view;
|
||||
}
|
||||
if ((int)decoded->PDOP) {
|
||||
jsonObj["payload"]["PDOP"] = (int)decoded->PDOP;
|
||||
}
|
||||
if ((int)decoded->HDOP) {
|
||||
jsonObj["payload"]["HDOP"] = (int)decoded->HDOP;
|
||||
}
|
||||
if ((int)decoded->VDOP) {
|
||||
jsonObj["payload"]["VDOP"] = (int)decoded->VDOP;
|
||||
}
|
||||
if ((int)decoded->precision_bits) {
|
||||
jsonObj["payload"]["precision_bits"] = (int)decoded->precision_bits;
|
||||
}
|
||||
} else if (shouldLog) {
|
||||
LOG_ERROR("Error decoding proto for position message!");
|
||||
return "";
|
||||
}
|
||||
break;
|
||||
}
|
||||
case meshtastic_PortNum_WAYPOINT_APP: {
|
||||
msgType = "position";
|
||||
meshtastic_Waypoint scratch;
|
||||
meshtastic_Waypoint *decoded = NULL;
|
||||
memset(&scratch, 0, sizeof(scratch));
|
||||
if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Waypoint_msg, &scratch)) {
|
||||
decoded = &scratch;
|
||||
jsonObj["payload"]["id"] = (unsigned int)decoded->id;
|
||||
jsonObj["payload"]["name"] = decoded->name;
|
||||
jsonObj["payload"]["description"] = decoded->description;
|
||||
jsonObj["payload"]["expire"] = (unsigned int)decoded->expire;
|
||||
jsonObj["payload"]["locked_to"] = (unsigned int)decoded->locked_to;
|
||||
jsonObj["payload"]["latitude_i"] = (int)decoded->latitude_i;
|
||||
jsonObj["payload"]["longitude_i"] = (int)decoded->longitude_i;
|
||||
} else if (shouldLog) {
|
||||
LOG_ERROR("Error decoding proto for position message!");
|
||||
return "";
|
||||
}
|
||||
break;
|
||||
}
|
||||
case meshtastic_PortNum_NEIGHBORINFO_APP: {
|
||||
msgType = "neighborinfo";
|
||||
meshtastic_NeighborInfo scratch;
|
||||
meshtastic_NeighborInfo *decoded = NULL;
|
||||
memset(&scratch, 0, sizeof(scratch));
|
||||
if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_NeighborInfo_msg,
|
||||
&scratch)) {
|
||||
decoded = &scratch;
|
||||
jsonObj["payload"]["node_id"] = (unsigned int)decoded->node_id;
|
||||
jsonObj["payload"]["node_broadcast_interval_secs"] = (unsigned int)decoded->node_broadcast_interval_secs;
|
||||
jsonObj["payload"]["last_sent_by_id"] = (unsigned int)decoded->last_sent_by_id;
|
||||
jsonObj["payload"]["neighbors_count"] = decoded->neighbors_count;
|
||||
|
||||
JsonObject neighbors_obj = arrayObj.to<JsonObject>();
|
||||
JsonArray neighbors = neighbors_obj.createNestedArray("neighbors");
|
||||
JsonObject neighbors_0 = neighbors.createNestedObject();
|
||||
|
||||
for (uint8_t i = 0; i < decoded->neighbors_count; i++) {
|
||||
neighbors_0["node_id"] = (unsigned int)decoded->neighbors[i].node_id;
|
||||
neighbors_0["snr"] = (int)decoded->neighbors[i].snr;
|
||||
neighbors[i + 1] = neighbors_0;
|
||||
neighbors_0.clear();
|
||||
}
|
||||
neighbors.remove(0);
|
||||
jsonObj["payload"]["neighbors"] = neighbors;
|
||||
} else if (shouldLog) {
|
||||
LOG_ERROR("Error decoding proto for neighborinfo message!");
|
||||
return "";
|
||||
}
|
||||
break;
|
||||
}
|
||||
case meshtastic_PortNum_TRACEROUTE_APP: {
|
||||
if (mp->decoded.request_id) { // Only report the traceroute response
|
||||
msgType = "traceroute";
|
||||
meshtastic_RouteDiscovery scratch;
|
||||
meshtastic_RouteDiscovery *decoded = NULL;
|
||||
memset(&scratch, 0, sizeof(scratch));
|
||||
if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_RouteDiscovery_msg,
|
||||
&scratch)) {
|
||||
decoded = &scratch;
|
||||
JsonArray route = arrayObj.createNestedArray("route");
|
||||
|
||||
auto addToRoute = [](JsonArray *route, NodeNum num) {
|
||||
char long_name[40] = "Unknown";
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num);
|
||||
bool name_known = node ? node->has_user : false;
|
||||
if (name_known)
|
||||
memcpy(long_name, node->user.long_name, sizeof(long_name));
|
||||
route->add(long_name);
|
||||
};
|
||||
|
||||
addToRoute(&route, mp->to); // route.add(mp->to);
|
||||
for (uint8_t i = 0; i < decoded->route_count; i++) {
|
||||
addToRoute(&route, decoded->route[i]); // route.add(decoded->route[i]);
|
||||
}
|
||||
addToRoute(&route,
|
||||
mp->from); // route.add(mp->from); // Ended at the original destination (source of response)
|
||||
|
||||
jsonObj["payload"]["route"] = route;
|
||||
} else if (shouldLog) {
|
||||
LOG_ERROR("Error decoding proto for traceroute message!");
|
||||
return "";
|
||||
}
|
||||
} else {
|
||||
LOG_WARN("Traceroute response not reported");
|
||||
return "";
|
||||
}
|
||||
break;
|
||||
}
|
||||
case meshtastic_PortNum_DETECTION_SENSOR_APP: {
|
||||
msgType = "detection";
|
||||
char payloadStr[(mp->decoded.payload.size) + 1];
|
||||
memcpy(payloadStr, mp->decoded.payload.bytes, mp->decoded.payload.size);
|
||||
payloadStr[mp->decoded.payload.size] = 0; // null terminated string
|
||||
jsonObj["payload"]["text"] = payloadStr;
|
||||
break;
|
||||
}
|
||||
case meshtastic_PortNum_REMOTE_HARDWARE_APP: {
|
||||
meshtastic_HardwareMessage scratch;
|
||||
meshtastic_HardwareMessage *decoded = NULL;
|
||||
memset(&scratch, 0, sizeof(scratch));
|
||||
if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_HardwareMessage_msg,
|
||||
&scratch)) {
|
||||
decoded = &scratch;
|
||||
if (decoded->type == meshtastic_HardwareMessage_Type_GPIOS_CHANGED) {
|
||||
msgType = "gpios_changed";
|
||||
jsonObj["payload"]["gpio_value"] = (unsigned int)decoded->gpio_value;
|
||||
} else if (decoded->type == meshtastic_HardwareMessage_Type_READ_GPIOS_REPLY) {
|
||||
msgType = "gpios_read_reply";
|
||||
jsonObj["payload"]["gpio_value"] = (unsigned int)decoded->gpio_value;
|
||||
jsonObj["payload"]["gpio_mask"] = (unsigned int)decoded->gpio_mask;
|
||||
}
|
||||
} else if (shouldLog) {
|
||||
LOG_ERROR("Error decoding proto for RemoteHardware message!");
|
||||
return "";
|
||||
}
|
||||
break;
|
||||
}
|
||||
// add more packet types here if needed
|
||||
default:
|
||||
LOG_WARN("Unsupported packet type %d", mp->decoded.portnum);
|
||||
return "";
|
||||
break;
|
||||
}
|
||||
} else if (shouldLog) {
|
||||
LOG_WARN("Couldn't convert encrypted payload of MeshPacket to JSON");
|
||||
return "";
|
||||
}
|
||||
|
||||
jsonObj["id"] = (unsigned int)mp->id;
|
||||
jsonObj["timestamp"] = (unsigned int)mp->rx_time;
|
||||
jsonObj["to"] = (unsigned int)mp->to;
|
||||
jsonObj["from"] = (unsigned int)mp->from;
|
||||
jsonObj["channel"] = (unsigned int)mp->channel;
|
||||
jsonObj["type"] = msgType.c_str();
|
||||
jsonObj["sender"] = nodeDB->getNodeId().c_str();
|
||||
if (mp->rx_rssi != 0)
|
||||
jsonObj["rssi"] = (int)mp->rx_rssi;
|
||||
if (mp->rx_snr != 0)
|
||||
jsonObj["snr"] = (float)mp->rx_snr;
|
||||
const int8_t hopsAway = getHopsAway(*mp);
|
||||
if (hopsAway >= 0) {
|
||||
jsonObj["hops_away"] = (unsigned int)(hopsAway);
|
||||
jsonObj["hop_start"] = (unsigned int)(mp->hop_start);
|
||||
}
|
||||
|
||||
// serialize and write it to the stream
|
||||
|
||||
// Serial.printf("serialized json message: \r");
|
||||
// serializeJson(jsonObj, Serial);
|
||||
// Serial.println("");
|
||||
|
||||
std::string jsonStr = "";
|
||||
serializeJson(jsonObj, jsonStr);
|
||||
|
||||
if (shouldLog)
|
||||
LOG_INFO("serialized json message: %s", jsonStr.c_str());
|
||||
|
||||
return jsonStr;
|
||||
}
|
||||
|
||||
std::string MeshPacketSerializer::JsonSerializeEncrypted(const meshtastic_MeshPacket *mp)
|
||||
{
|
||||
jsonObj.clear();
|
||||
jsonObj["id"] = (unsigned int)mp->id;
|
||||
jsonObj["time_ms"] = (double)millis();
|
||||
jsonObj["timestamp"] = (unsigned int)mp->rx_time;
|
||||
jsonObj["to"] = (unsigned int)mp->to;
|
||||
jsonObj["from"] = (unsigned int)mp->from;
|
||||
jsonObj["channel"] = (unsigned int)mp->channel;
|
||||
jsonObj["want_ack"] = mp->want_ack;
|
||||
|
||||
if (mp->rx_rssi != 0)
|
||||
jsonObj["rssi"] = (int)mp->rx_rssi;
|
||||
if (mp->rx_snr != 0)
|
||||
jsonObj["snr"] = (float)mp->rx_snr;
|
||||
const int8_t hopsAway = getHopsAway(*mp);
|
||||
if (hopsAway >= 0) {
|
||||
jsonObj["hops_away"] = (unsigned int)(hopsAway);
|
||||
jsonObj["hop_start"] = (unsigned int)(mp->hop_start);
|
||||
}
|
||||
jsonObj["size"] = (unsigned int)mp->encrypted.size;
|
||||
auto encryptedStr = bytesToHex(mp->encrypted.bytes, mp->encrypted.size);
|
||||
jsonObj["bytes"] = encryptedStr.c_str();
|
||||
|
||||
// serialize and write it to the stream
|
||||
std::string jsonStr = "";
|
||||
serializeJson(jsonObj, jsonStr);
|
||||
|
||||
return jsonStr;
|
||||
}
|
||||
#endif
|
||||
@@ -1,42 +1,31 @@
|
||||
#include "../test_helpers.h"
|
||||
|
||||
// Helper function for all encrypted packet assertions
|
||||
void assert_encrypted_packet(const std::string &json, meshtastic_MeshPacket packet)
|
||||
static void assert_encrypted_packet(const std::string &json, const meshtastic_MeshPacket &packet)
|
||||
{
|
||||
// Parse and validate JSON
|
||||
TEST_ASSERT_TRUE(json.length() > 0);
|
||||
|
||||
JSONValue *root = JSON::Parse(json.c_str());
|
||||
TEST_ASSERT_NOT_NULL(root);
|
||||
TEST_ASSERT_TRUE(root->IsObject());
|
||||
Json::Value root = parse_json(json);
|
||||
TEST_ASSERT_TRUE(root.isObject());
|
||||
|
||||
JSONObject jsonObj = root->AsObject();
|
||||
TEST_ASSERT_TRUE(root.isMember("from"));
|
||||
TEST_ASSERT_EQUAL(packet.from, root["from"].asUInt());
|
||||
|
||||
// Assert basic packet fields
|
||||
TEST_ASSERT_TRUE(jsonObj.find("from") != jsonObj.end());
|
||||
TEST_ASSERT_EQUAL(packet.from, (uint32_t)jsonObj.at("from")->AsNumber());
|
||||
TEST_ASSERT_TRUE(root.isMember("to"));
|
||||
TEST_ASSERT_EQUAL(packet.to, root["to"].asUInt());
|
||||
|
||||
TEST_ASSERT_TRUE(jsonObj.find("to") != jsonObj.end());
|
||||
TEST_ASSERT_EQUAL(packet.to, (uint32_t)jsonObj.at("to")->AsNumber());
|
||||
TEST_ASSERT_TRUE(root.isMember("id"));
|
||||
TEST_ASSERT_EQUAL(packet.id, root["id"].asUInt());
|
||||
|
||||
TEST_ASSERT_TRUE(jsonObj.find("id") != jsonObj.end());
|
||||
TEST_ASSERT_EQUAL(packet.id, (uint32_t)jsonObj.at("id")->AsNumber());
|
||||
TEST_ASSERT_TRUE(root.isMember("bytes"));
|
||||
TEST_ASSERT_TRUE(root["bytes"].isString());
|
||||
|
||||
// Assert encrypted data fields
|
||||
TEST_ASSERT_TRUE(jsonObj.find("bytes") != jsonObj.end());
|
||||
TEST_ASSERT_TRUE(jsonObj.at("bytes")->IsString());
|
||||
TEST_ASSERT_TRUE(root.isMember("size"));
|
||||
TEST_ASSERT_EQUAL(packet.encrypted.size, (int)root["size"].asInt());
|
||||
|
||||
TEST_ASSERT_TRUE(jsonObj.find("size") != jsonObj.end());
|
||||
TEST_ASSERT_EQUAL(packet.encrypted.size, (int)jsonObj.at("size")->AsNumber());
|
||||
|
||||
// Assert hex encoding
|
||||
std::string encrypted_hex = jsonObj["bytes"]->AsString();
|
||||
std::string encrypted_hex = root["bytes"].asString();
|
||||
TEST_ASSERT_EQUAL(packet.encrypted.size * 2, encrypted_hex.length());
|
||||
|
||||
delete root;
|
||||
}
|
||||
|
||||
// Test encrypted packet serialization
|
||||
void test_encrypted_packet_serialization()
|
||||
{
|
||||
const char *data = "encrypted_payload_data";
|
||||
@@ -48,7 +37,6 @@ void test_encrypted_packet_serialization()
|
||||
assert_encrypted_packet(json, packet);
|
||||
}
|
||||
|
||||
// Test empty encrypted packet
|
||||
void test_empty_encrypted_packet()
|
||||
{
|
||||
meshtastic_MeshPacket packet =
|
||||
|
||||
@@ -13,7 +13,6 @@ static size_t encode_user_info(uint8_t *buffer, size_t buffer_size)
|
||||
return stream.bytes_written;
|
||||
}
|
||||
|
||||
// Test NODEINFO_APP port
|
||||
void test_nodeinfo_serialization()
|
||||
{
|
||||
uint8_t buffer[256];
|
||||
@@ -24,28 +23,20 @@ void test_nodeinfo_serialization()
|
||||
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
|
||||
TEST_ASSERT_TRUE(json.length() > 0);
|
||||
|
||||
JSONValue *root = JSON::Parse(json.c_str());
|
||||
TEST_ASSERT_NOT_NULL(root);
|
||||
TEST_ASSERT_TRUE(root->IsObject());
|
||||
Json::Value root = parse_json(json);
|
||||
TEST_ASSERT_TRUE(root.isObject());
|
||||
|
||||
JSONObject jsonObj = root->AsObject();
|
||||
TEST_ASSERT_TRUE(root.isMember("type"));
|
||||
TEST_ASSERT_EQUAL_STRING("nodeinfo", root["type"].asString().c_str());
|
||||
|
||||
// Check message type
|
||||
TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end());
|
||||
TEST_ASSERT_EQUAL_STRING("nodeinfo", jsonObj["type"]->AsString().c_str());
|
||||
TEST_ASSERT_TRUE(root.isMember("payload"));
|
||||
TEST_ASSERT_TRUE(root["payload"].isObject());
|
||||
|
||||
// Check payload
|
||||
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
|
||||
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
|
||||
const Json::Value &payload = root["payload"];
|
||||
|
||||
JSONObject payload = jsonObj["payload"]->AsObject();
|
||||
TEST_ASSERT_TRUE(payload.isMember("shortname"));
|
||||
TEST_ASSERT_EQUAL_STRING("TEST", payload["shortname"].asString().c_str());
|
||||
|
||||
// Verify user data
|
||||
TEST_ASSERT_TRUE(payload.find("shortname") != payload.end());
|
||||
TEST_ASSERT_EQUAL_STRING("TEST", payload["shortname"]->AsString().c_str());
|
||||
|
||||
TEST_ASSERT_TRUE(payload.find("longname") != payload.end());
|
||||
TEST_ASSERT_EQUAL_STRING("Test User", payload["longname"]->AsString().c_str());
|
||||
|
||||
delete root;
|
||||
TEST_ASSERT_TRUE(payload.isMember("longname"));
|
||||
TEST_ASSERT_EQUAL_STRING("Test User", payload["longname"].asString().c_str());
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
static size_t encode_position(uint8_t *buffer, size_t buffer_size)
|
||||
{
|
||||
meshtastic_Position position = meshtastic_Position_init_zero;
|
||||
position.latitude_i = 374208000; // 37.4208 degrees * 1e7
|
||||
position.longitude_i = -1221981000; // -122.1981 degrees * 1e7
|
||||
position.latitude_i = 374208000;
|
||||
position.longitude_i = -1221981000;
|
||||
position.altitude = 123;
|
||||
position.time = 1609459200;
|
||||
position.has_altitude = true;
|
||||
@@ -16,7 +16,6 @@ static size_t encode_position(uint8_t *buffer, size_t buffer_size)
|
||||
return stream.bytes_written;
|
||||
}
|
||||
|
||||
// Test POSITION_APP port
|
||||
void test_position_serialization()
|
||||
{
|
||||
uint8_t buffer[256];
|
||||
@@ -27,31 +26,23 @@ void test_position_serialization()
|
||||
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
|
||||
TEST_ASSERT_TRUE(json.length() > 0);
|
||||
|
||||
JSONValue *root = JSON::Parse(json.c_str());
|
||||
TEST_ASSERT_NOT_NULL(root);
|
||||
TEST_ASSERT_TRUE(root->IsObject());
|
||||
Json::Value root = parse_json(json);
|
||||
TEST_ASSERT_TRUE(root.isObject());
|
||||
|
||||
JSONObject jsonObj = root->AsObject();
|
||||
TEST_ASSERT_TRUE(root.isMember("type"));
|
||||
TEST_ASSERT_EQUAL_STRING("position", root["type"].asString().c_str());
|
||||
|
||||
// Check message type
|
||||
TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end());
|
||||
TEST_ASSERT_EQUAL_STRING("position", jsonObj["type"]->AsString().c_str());
|
||||
TEST_ASSERT_TRUE(root.isMember("payload"));
|
||||
TEST_ASSERT_TRUE(root["payload"].isObject());
|
||||
|
||||
// Check payload
|
||||
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
|
||||
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
|
||||
const Json::Value &payload = root["payload"];
|
||||
|
||||
JSONObject payload = jsonObj["payload"]->AsObject();
|
||||
TEST_ASSERT_TRUE(payload.isMember("latitude_i"));
|
||||
TEST_ASSERT_EQUAL(374208000, payload["latitude_i"].asInt());
|
||||
|
||||
// Verify position data
|
||||
TEST_ASSERT_TRUE(payload.find("latitude_i") != payload.end());
|
||||
TEST_ASSERT_EQUAL(374208000, (int)payload["latitude_i"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.isMember("longitude_i"));
|
||||
TEST_ASSERT_EQUAL(-1221981000, payload["longitude_i"].asInt());
|
||||
|
||||
TEST_ASSERT_TRUE(payload.find("longitude_i") != payload.end());
|
||||
TEST_ASSERT_EQUAL(-1221981000, (int)payload["longitude_i"]->AsNumber());
|
||||
|
||||
TEST_ASSERT_TRUE(payload.find("altitude") != payload.end());
|
||||
TEST_ASSERT_EQUAL(123, (int)payload["altitude"]->AsNumber());
|
||||
|
||||
delete root;
|
||||
TEST_ASSERT_TRUE(payload.isMember("altitude"));
|
||||
TEST_ASSERT_EQUAL(123, payload["altitude"].asInt());
|
||||
}
|
||||
|
||||
@@ -1,6 +1,62 @@
|
||||
#include "../test_helpers.h"
|
||||
|
||||
// Helper function to create and encode device metrics
|
||||
static void fill_all_env_metrics(meshtastic_Telemetry &telemetry)
|
||||
{
|
||||
telemetry.variant.environment_metrics.temperature = 23.56f;
|
||||
telemetry.variant.environment_metrics.has_temperature = true;
|
||||
telemetry.variant.environment_metrics.relative_humidity = 65.43f;
|
||||
telemetry.variant.environment_metrics.has_relative_humidity = true;
|
||||
telemetry.variant.environment_metrics.barometric_pressure = 1013.27f;
|
||||
telemetry.variant.environment_metrics.has_barometric_pressure = true;
|
||||
|
||||
telemetry.variant.environment_metrics.gas_resistance = 50.58f;
|
||||
telemetry.variant.environment_metrics.has_gas_resistance = true;
|
||||
telemetry.variant.environment_metrics.iaq = 120;
|
||||
telemetry.variant.environment_metrics.has_iaq = true;
|
||||
|
||||
telemetry.variant.environment_metrics.voltage = 3.34f;
|
||||
telemetry.variant.environment_metrics.has_voltage = true;
|
||||
telemetry.variant.environment_metrics.current = 0.53f;
|
||||
telemetry.variant.environment_metrics.has_current = true;
|
||||
|
||||
telemetry.variant.environment_metrics.lux = 450.12f;
|
||||
telemetry.variant.environment_metrics.has_lux = true;
|
||||
telemetry.variant.environment_metrics.white_lux = 380.95f;
|
||||
telemetry.variant.environment_metrics.has_white_lux = true;
|
||||
telemetry.variant.environment_metrics.ir_lux = 25.37f;
|
||||
telemetry.variant.environment_metrics.has_ir_lux = true;
|
||||
telemetry.variant.environment_metrics.uv_lux = 15.68f;
|
||||
telemetry.variant.environment_metrics.has_uv_lux = true;
|
||||
|
||||
telemetry.variant.environment_metrics.distance = 150.29f;
|
||||
telemetry.variant.environment_metrics.has_distance = true;
|
||||
|
||||
telemetry.variant.environment_metrics.wind_direction = 180;
|
||||
telemetry.variant.environment_metrics.has_wind_direction = true;
|
||||
telemetry.variant.environment_metrics.wind_speed = 5.52f;
|
||||
telemetry.variant.environment_metrics.has_wind_speed = true;
|
||||
telemetry.variant.environment_metrics.wind_gust = 8.24f;
|
||||
telemetry.variant.environment_metrics.has_wind_gust = true;
|
||||
telemetry.variant.environment_metrics.wind_lull = 2.13f;
|
||||
telemetry.variant.environment_metrics.has_wind_lull = true;
|
||||
|
||||
telemetry.variant.environment_metrics.weight = 75.56f;
|
||||
telemetry.variant.environment_metrics.has_weight = true;
|
||||
|
||||
telemetry.variant.environment_metrics.radiation = 0.13f;
|
||||
telemetry.variant.environment_metrics.has_radiation = true;
|
||||
|
||||
telemetry.variant.environment_metrics.rainfall_1h = 2.57f;
|
||||
telemetry.variant.environment_metrics.has_rainfall_1h = true;
|
||||
telemetry.variant.environment_metrics.rainfall_24h = 15.89f;
|
||||
telemetry.variant.environment_metrics.has_rainfall_24h = true;
|
||||
|
||||
telemetry.variant.environment_metrics.soil_moisture = 85;
|
||||
telemetry.variant.environment_metrics.has_soil_moisture = true;
|
||||
telemetry.variant.environment_metrics.soil_temperature = 18.54f;
|
||||
telemetry.variant.environment_metrics.has_soil_temperature = true;
|
||||
}
|
||||
|
||||
static size_t encode_telemetry_device_metrics(uint8_t *buffer, size_t buffer_size)
|
||||
{
|
||||
meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero;
|
||||
@@ -22,507 +78,233 @@ static size_t encode_telemetry_device_metrics(uint8_t *buffer, size_t buffer_siz
|
||||
return stream.bytes_written;
|
||||
}
|
||||
|
||||
// Helper function to create and encode empty environment metrics (no fields set)
|
||||
static size_t encode_telemetry_environment_metrics_empty(uint8_t *buffer, size_t buffer_size)
|
||||
{
|
||||
meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero;
|
||||
telemetry.time = 1609459200;
|
||||
telemetry.which_variant = meshtastic_Telemetry_environment_metrics_tag;
|
||||
|
||||
// NO fields are set - all has_* flags remain false
|
||||
// This tests that empty environment metrics don't produce any JSON fields
|
||||
|
||||
pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size);
|
||||
pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry);
|
||||
return stream.bytes_written;
|
||||
}
|
||||
|
||||
// Helper function to create environment metrics with ALL possible fields set
|
||||
// This function should be updated whenever new fields are added to the protobuf
|
||||
static size_t encode_telemetry_environment_metrics_all_fields(uint8_t *buffer, size_t buffer_size)
|
||||
{
|
||||
meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero;
|
||||
telemetry.time = 1609459200;
|
||||
telemetry.which_variant = meshtastic_Telemetry_environment_metrics_tag;
|
||||
|
||||
// Basic environment metrics
|
||||
telemetry.variant.environment_metrics.temperature = 23.56f;
|
||||
telemetry.variant.environment_metrics.has_temperature = true;
|
||||
telemetry.variant.environment_metrics.relative_humidity = 65.43f;
|
||||
telemetry.variant.environment_metrics.has_relative_humidity = true;
|
||||
telemetry.variant.environment_metrics.barometric_pressure = 1013.27f;
|
||||
telemetry.variant.environment_metrics.has_barometric_pressure = true;
|
||||
|
||||
// Gas and air quality
|
||||
telemetry.variant.environment_metrics.gas_resistance = 50.58f;
|
||||
telemetry.variant.environment_metrics.has_gas_resistance = true;
|
||||
telemetry.variant.environment_metrics.iaq = 120;
|
||||
telemetry.variant.environment_metrics.has_iaq = true;
|
||||
|
||||
// Power measurements
|
||||
telemetry.variant.environment_metrics.voltage = 3.34f;
|
||||
telemetry.variant.environment_metrics.has_voltage = true;
|
||||
telemetry.variant.environment_metrics.current = 0.53f;
|
||||
telemetry.variant.environment_metrics.has_current = true;
|
||||
|
||||
// Light measurements (ALL 4 types)
|
||||
telemetry.variant.environment_metrics.lux = 450.12f;
|
||||
telemetry.variant.environment_metrics.has_lux = true;
|
||||
telemetry.variant.environment_metrics.white_lux = 380.95f;
|
||||
telemetry.variant.environment_metrics.has_white_lux = true;
|
||||
telemetry.variant.environment_metrics.ir_lux = 25.37f;
|
||||
telemetry.variant.environment_metrics.has_ir_lux = true;
|
||||
telemetry.variant.environment_metrics.uv_lux = 15.68f;
|
||||
telemetry.variant.environment_metrics.has_uv_lux = true;
|
||||
|
||||
// Distance measurement
|
||||
telemetry.variant.environment_metrics.distance = 150.29f;
|
||||
telemetry.variant.environment_metrics.has_distance = true;
|
||||
|
||||
// Wind measurements (ALL 4 types)
|
||||
telemetry.variant.environment_metrics.wind_direction = 180;
|
||||
telemetry.variant.environment_metrics.has_wind_direction = true;
|
||||
telemetry.variant.environment_metrics.wind_speed = 5.52f;
|
||||
telemetry.variant.environment_metrics.has_wind_speed = true;
|
||||
telemetry.variant.environment_metrics.wind_gust = 8.24f;
|
||||
telemetry.variant.environment_metrics.has_wind_gust = true;
|
||||
telemetry.variant.environment_metrics.wind_lull = 2.13f;
|
||||
telemetry.variant.environment_metrics.has_wind_lull = true;
|
||||
|
||||
// Weight measurement
|
||||
telemetry.variant.environment_metrics.weight = 75.56f;
|
||||
telemetry.variant.environment_metrics.has_weight = true;
|
||||
|
||||
// Radiation measurement
|
||||
telemetry.variant.environment_metrics.radiation = 0.13f;
|
||||
telemetry.variant.environment_metrics.has_radiation = true;
|
||||
|
||||
// Rainfall measurements (BOTH types)
|
||||
telemetry.variant.environment_metrics.rainfall_1h = 2.57f;
|
||||
telemetry.variant.environment_metrics.has_rainfall_1h = true;
|
||||
telemetry.variant.environment_metrics.rainfall_24h = 15.89f;
|
||||
telemetry.variant.environment_metrics.has_rainfall_24h = true;
|
||||
|
||||
// Soil measurements (BOTH types)
|
||||
telemetry.variant.environment_metrics.soil_moisture = 85;
|
||||
telemetry.variant.environment_metrics.has_soil_moisture = true;
|
||||
telemetry.variant.environment_metrics.soil_temperature = 18.54f;
|
||||
telemetry.variant.environment_metrics.has_soil_temperature = true;
|
||||
|
||||
// IMPORTANT: When new environment fields are added to the protobuf,
|
||||
// they MUST be added here too, or the coverage test will fail!
|
||||
|
||||
pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size);
|
||||
pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry);
|
||||
return stream.bytes_written;
|
||||
}
|
||||
|
||||
// Helper function to create and encode environment metrics with all current fields
|
||||
static size_t encode_telemetry_environment_metrics(uint8_t *buffer, size_t buffer_size)
|
||||
{
|
||||
meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero;
|
||||
telemetry.time = 1609459200;
|
||||
telemetry.which_variant = meshtastic_Telemetry_environment_metrics_tag;
|
||||
|
||||
// Basic environment metrics
|
||||
telemetry.variant.environment_metrics.temperature = 23.56f;
|
||||
telemetry.variant.environment_metrics.has_temperature = true;
|
||||
telemetry.variant.environment_metrics.relative_humidity = 65.43f;
|
||||
telemetry.variant.environment_metrics.has_relative_humidity = true;
|
||||
telemetry.variant.environment_metrics.barometric_pressure = 1013.27f;
|
||||
telemetry.variant.environment_metrics.has_barometric_pressure = true;
|
||||
|
||||
// Gas and air quality
|
||||
telemetry.variant.environment_metrics.gas_resistance = 50.58f;
|
||||
telemetry.variant.environment_metrics.has_gas_resistance = true;
|
||||
telemetry.variant.environment_metrics.iaq = 120;
|
||||
telemetry.variant.environment_metrics.has_iaq = true;
|
||||
|
||||
// Power measurements
|
||||
telemetry.variant.environment_metrics.voltage = 3.34f;
|
||||
telemetry.variant.environment_metrics.has_voltage = true;
|
||||
telemetry.variant.environment_metrics.current = 0.53f;
|
||||
telemetry.variant.environment_metrics.has_current = true;
|
||||
|
||||
// Light measurements
|
||||
telemetry.variant.environment_metrics.lux = 450.12f;
|
||||
telemetry.variant.environment_metrics.has_lux = true;
|
||||
telemetry.variant.environment_metrics.white_lux = 380.95f;
|
||||
telemetry.variant.environment_metrics.has_white_lux = true;
|
||||
telemetry.variant.environment_metrics.ir_lux = 25.37f;
|
||||
telemetry.variant.environment_metrics.has_ir_lux = true;
|
||||
telemetry.variant.environment_metrics.uv_lux = 15.68f;
|
||||
telemetry.variant.environment_metrics.has_uv_lux = true;
|
||||
|
||||
// Distance measurement
|
||||
telemetry.variant.environment_metrics.distance = 150.29f;
|
||||
telemetry.variant.environment_metrics.has_distance = true;
|
||||
|
||||
// Wind measurements
|
||||
telemetry.variant.environment_metrics.wind_direction = 180;
|
||||
telemetry.variant.environment_metrics.has_wind_direction = true;
|
||||
telemetry.variant.environment_metrics.wind_speed = 5.52f;
|
||||
telemetry.variant.environment_metrics.has_wind_speed = true;
|
||||
telemetry.variant.environment_metrics.wind_gust = 8.24f;
|
||||
telemetry.variant.environment_metrics.has_wind_gust = true;
|
||||
telemetry.variant.environment_metrics.wind_lull = 2.13f;
|
||||
telemetry.variant.environment_metrics.has_wind_lull = true;
|
||||
|
||||
// Weight measurement
|
||||
telemetry.variant.environment_metrics.weight = 75.56f;
|
||||
telemetry.variant.environment_metrics.has_weight = true;
|
||||
|
||||
// Radiation measurement
|
||||
telemetry.variant.environment_metrics.radiation = 0.13f;
|
||||
telemetry.variant.environment_metrics.has_radiation = true;
|
||||
|
||||
// Rainfall measurements
|
||||
telemetry.variant.environment_metrics.rainfall_1h = 2.57f;
|
||||
telemetry.variant.environment_metrics.has_rainfall_1h = true;
|
||||
telemetry.variant.environment_metrics.rainfall_24h = 15.89f;
|
||||
telemetry.variant.environment_metrics.has_rainfall_24h = true;
|
||||
|
||||
// Soil measurements
|
||||
telemetry.variant.environment_metrics.soil_moisture = 85;
|
||||
telemetry.variant.environment_metrics.has_soil_moisture = true;
|
||||
telemetry.variant.environment_metrics.soil_temperature = 18.54f;
|
||||
telemetry.variant.environment_metrics.has_soil_temperature = true;
|
||||
fill_all_env_metrics(telemetry);
|
||||
|
||||
pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size);
|
||||
pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry);
|
||||
return stream.bytes_written;
|
||||
}
|
||||
|
||||
// Test TELEMETRY_APP port with device metrics
|
||||
static Json::Value serialize_and_get_payload(meshtastic_PortNum port, const uint8_t *buffer, size_t payload_size)
|
||||
{
|
||||
meshtastic_MeshPacket packet = create_test_packet(port, buffer, payload_size);
|
||||
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
|
||||
TEST_ASSERT_TRUE(json.length() > 0);
|
||||
|
||||
Json::Value root = parse_json(json);
|
||||
TEST_ASSERT_TRUE(root.isObject());
|
||||
TEST_ASSERT_TRUE(root.isMember("payload"));
|
||||
TEST_ASSERT_TRUE(root["payload"].isObject());
|
||||
return root;
|
||||
}
|
||||
|
||||
void test_telemetry_device_metrics_serialization()
|
||||
{
|
||||
uint8_t buffer[256];
|
||||
size_t payload_size = encode_telemetry_device_metrics(buffer, sizeof(buffer));
|
||||
|
||||
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
|
||||
Json::Value root = serialize_and_get_payload(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
|
||||
|
||||
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
|
||||
TEST_ASSERT_TRUE(json.length() > 0);
|
||||
TEST_ASSERT_TRUE(root.isMember("type"));
|
||||
TEST_ASSERT_EQUAL_STRING("telemetry", root["type"].asString().c_str());
|
||||
|
||||
JSONValue *root = JSON::Parse(json.c_str());
|
||||
TEST_ASSERT_NOT_NULL(root);
|
||||
TEST_ASSERT_TRUE(root->IsObject());
|
||||
const Json::Value &payload = root["payload"];
|
||||
|
||||
JSONObject jsonObj = root->AsObject();
|
||||
TEST_ASSERT_TRUE(payload.isMember("battery_level"));
|
||||
TEST_ASSERT_EQUAL(85, payload["battery_level"].asInt());
|
||||
|
||||
// Check message type
|
||||
TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end());
|
||||
TEST_ASSERT_EQUAL_STRING("telemetry", jsonObj["type"]->AsString().c_str());
|
||||
TEST_ASSERT_TRUE(payload.isMember("voltage"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 3.72f, payload["voltage"].asFloat());
|
||||
|
||||
// Check payload
|
||||
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
|
||||
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
|
||||
TEST_ASSERT_TRUE(payload.isMember("channel_utilization"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.56f, payload["channel_utilization"].asFloat());
|
||||
|
||||
JSONObject payload = jsonObj["payload"]->AsObject();
|
||||
|
||||
// Verify telemetry data
|
||||
TEST_ASSERT_TRUE(payload.find("battery_level") != payload.end());
|
||||
TEST_ASSERT_EQUAL(85, (int)payload["battery_level"]->AsNumber());
|
||||
|
||||
TEST_ASSERT_TRUE(payload.find("voltage") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 3.72f, payload["voltage"]->AsNumber());
|
||||
|
||||
TEST_ASSERT_TRUE(payload.find("channel_utilization") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.56f, payload["channel_utilization"]->AsNumber());
|
||||
|
||||
TEST_ASSERT_TRUE(payload.find("uptime_seconds") != payload.end());
|
||||
TEST_ASSERT_EQUAL(12345, (int)payload["uptime_seconds"]->AsNumber());
|
||||
|
||||
// Note: JSON serialization may not preserve exact 2-decimal formatting due to float precision
|
||||
// We verify the numeric values are correct within tolerance
|
||||
|
||||
delete root;
|
||||
TEST_ASSERT_TRUE(payload.isMember("uptime_seconds"));
|
||||
TEST_ASSERT_EQUAL(12345, payload["uptime_seconds"].asInt());
|
||||
}
|
||||
|
||||
// Test that telemetry environment metrics are properly serialized
|
||||
void test_telemetry_environment_metrics_serialization()
|
||||
{
|
||||
uint8_t buffer[256];
|
||||
size_t payload_size = encode_telemetry_environment_metrics(buffer, sizeof(buffer));
|
||||
|
||||
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
|
||||
Json::Value root = serialize_and_get_payload(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
|
||||
const Json::Value &payload = root["payload"];
|
||||
|
||||
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
|
||||
TEST_ASSERT_TRUE(json.length() > 0);
|
||||
TEST_ASSERT_TRUE(payload.isMember("temperature"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 23.56f, payload["temperature"].asFloat());
|
||||
|
||||
JSONValue *root = JSON::Parse(json.c_str());
|
||||
TEST_ASSERT_NOT_NULL(root);
|
||||
TEST_ASSERT_TRUE(root->IsObject());
|
||||
TEST_ASSERT_TRUE(payload.isMember("relative_humidity"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 65.43f, payload["relative_humidity"].asFloat());
|
||||
|
||||
JSONObject jsonObj = root->AsObject();
|
||||
|
||||
// Check payload exists
|
||||
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
|
||||
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
|
||||
|
||||
JSONObject payload = jsonObj["payload"]->AsObject();
|
||||
|
||||
// Test key fields that should be present in the serializer
|
||||
TEST_ASSERT_TRUE(payload.find("temperature") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 23.56f, payload["temperature"]->AsNumber());
|
||||
|
||||
TEST_ASSERT_TRUE(payload.find("relative_humidity") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 65.43f, payload["relative_humidity"]->AsNumber());
|
||||
|
||||
TEST_ASSERT_TRUE(payload.find("distance") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 150.29f, payload["distance"]->AsNumber());
|
||||
|
||||
// Note: JSON serialization may have float precision limitations
|
||||
// We focus on verifying numeric accuracy rather than exact string formatting
|
||||
|
||||
delete root;
|
||||
TEST_ASSERT_TRUE(payload.isMember("distance"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 150.29f, payload["distance"].asFloat());
|
||||
}
|
||||
|
||||
// Test comprehensive environment metrics coverage
|
||||
void test_telemetry_environment_metrics_comprehensive()
|
||||
{
|
||||
uint8_t buffer[256];
|
||||
size_t payload_size = encode_telemetry_environment_metrics(buffer, sizeof(buffer));
|
||||
|
||||
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
|
||||
Json::Value root = serialize_and_get_payload(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
|
||||
const Json::Value &payload = root["payload"];
|
||||
|
||||
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
|
||||
TEST_ASSERT_TRUE(json.length() > 0);
|
||||
|
||||
JSONValue *root = JSON::Parse(json.c_str());
|
||||
TEST_ASSERT_NOT_NULL(root);
|
||||
TEST_ASSERT_TRUE(root->IsObject());
|
||||
|
||||
JSONObject jsonObj = root->AsObject();
|
||||
|
||||
// Check payload exists
|
||||
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
|
||||
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
|
||||
|
||||
JSONObject payload = jsonObj["payload"]->AsObject();
|
||||
|
||||
// Check all 15 originally supported fields
|
||||
TEST_ASSERT_TRUE(payload.find("temperature") != payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("relative_humidity") != payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("barometric_pressure") != payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("gas_resistance") != payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("voltage") != payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("current") != payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("iaq") != payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("distance") != payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("lux") != payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("white_lux") != payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("wind_direction") != payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("wind_speed") != payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("wind_gust") != payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("wind_lull") != payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("radiation") != payload.end());
|
||||
|
||||
delete root;
|
||||
TEST_ASSERT_TRUE(payload.isMember("temperature"));
|
||||
TEST_ASSERT_TRUE(payload.isMember("relative_humidity"));
|
||||
TEST_ASSERT_TRUE(payload.isMember("barometric_pressure"));
|
||||
TEST_ASSERT_TRUE(payload.isMember("gas_resistance"));
|
||||
TEST_ASSERT_TRUE(payload.isMember("voltage"));
|
||||
TEST_ASSERT_TRUE(payload.isMember("current"));
|
||||
TEST_ASSERT_TRUE(payload.isMember("iaq"));
|
||||
TEST_ASSERT_TRUE(payload.isMember("distance"));
|
||||
TEST_ASSERT_TRUE(payload.isMember("lux"));
|
||||
TEST_ASSERT_TRUE(payload.isMember("white_lux"));
|
||||
TEST_ASSERT_TRUE(payload.isMember("wind_direction"));
|
||||
TEST_ASSERT_TRUE(payload.isMember("wind_speed"));
|
||||
TEST_ASSERT_TRUE(payload.isMember("wind_gust"));
|
||||
TEST_ASSERT_TRUE(payload.isMember("wind_lull"));
|
||||
TEST_ASSERT_TRUE(payload.isMember("radiation"));
|
||||
}
|
||||
|
||||
// Test for the 7 environment fields that were added to complete coverage
|
||||
void test_telemetry_environment_metrics_missing_fields()
|
||||
{
|
||||
uint8_t buffer[256];
|
||||
size_t payload_size = encode_telemetry_environment_metrics(buffer, sizeof(buffer));
|
||||
|
||||
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
|
||||
Json::Value root = serialize_and_get_payload(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
|
||||
const Json::Value &payload = root["payload"];
|
||||
|
||||
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
|
||||
TEST_ASSERT_TRUE(json.length() > 0);
|
||||
TEST_ASSERT_TRUE(payload.isMember("ir_lux"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 25.37f, payload["ir_lux"].asFloat());
|
||||
|
||||
JSONValue *root = JSON::Parse(json.c_str());
|
||||
TEST_ASSERT_NOT_NULL(root);
|
||||
TEST_ASSERT_TRUE(root->IsObject());
|
||||
TEST_ASSERT_TRUE(payload.isMember("uv_lux"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.68f, payload["uv_lux"].asFloat());
|
||||
|
||||
JSONObject jsonObj = root->AsObject();
|
||||
TEST_ASSERT_TRUE(payload.isMember("weight"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 75.56f, payload["weight"].asFloat());
|
||||
|
||||
// Check payload exists
|
||||
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
|
||||
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
|
||||
TEST_ASSERT_TRUE(payload.isMember("rainfall_1h"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.57f, payload["rainfall_1h"].asFloat());
|
||||
|
||||
JSONObject payload = jsonObj["payload"]->AsObject();
|
||||
TEST_ASSERT_TRUE(payload.isMember("rainfall_24h"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.89f, payload["rainfall_24h"].asFloat());
|
||||
|
||||
// Check the 7 fields that were previously missing
|
||||
TEST_ASSERT_TRUE(payload.find("ir_lux") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 25.37f, payload["ir_lux"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.isMember("soil_moisture"));
|
||||
TEST_ASSERT_EQUAL(85, payload["soil_moisture"].asInt());
|
||||
|
||||
TEST_ASSERT_TRUE(payload.find("uv_lux") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.68f, payload["uv_lux"]->AsNumber());
|
||||
|
||||
TEST_ASSERT_TRUE(payload.find("weight") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 75.56f, payload["weight"]->AsNumber());
|
||||
|
||||
TEST_ASSERT_TRUE(payload.find("rainfall_1h") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.57f, payload["rainfall_1h"]->AsNumber());
|
||||
|
||||
TEST_ASSERT_TRUE(payload.find("rainfall_24h") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.89f, payload["rainfall_24h"]->AsNumber());
|
||||
|
||||
TEST_ASSERT_TRUE(payload.find("soil_moisture") != payload.end());
|
||||
TEST_ASSERT_EQUAL(85, (int)payload["soil_moisture"]->AsNumber());
|
||||
|
||||
TEST_ASSERT_TRUE(payload.find("soil_temperature") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 18.54f, payload["soil_temperature"]->AsNumber());
|
||||
|
||||
// Note: JSON float serialization may not preserve exact decimal formatting
|
||||
// We verify the values are numerically correct within tolerance
|
||||
|
||||
delete root;
|
||||
TEST_ASSERT_TRUE(payload.isMember("soil_temperature"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 18.54f, payload["soil_temperature"].asFloat());
|
||||
}
|
||||
|
||||
// Test that ALL environment fields are serialized (canary test for forgotten fields)
|
||||
// This test will FAIL if a new environment field is added to the protobuf but not to the serializer
|
||||
// Canary test: if a new env field is added to the protobuf but not to the serializer
|
||||
// (or to fill_all_env_metrics), this test will fail.
|
||||
void test_telemetry_environment_metrics_complete_coverage()
|
||||
{
|
||||
uint8_t buffer[256];
|
||||
size_t payload_size = encode_telemetry_environment_metrics_all_fields(buffer, sizeof(buffer));
|
||||
size_t payload_size = encode_telemetry_environment_metrics(buffer, sizeof(buffer));
|
||||
|
||||
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
|
||||
Json::Value root = serialize_and_get_payload(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
|
||||
const Json::Value &payload = root["payload"];
|
||||
|
||||
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
|
||||
TEST_ASSERT_TRUE(json.length() > 0);
|
||||
TEST_ASSERT_TRUE(payload.isMember("temperature"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 23.56f, payload["temperature"].asFloat());
|
||||
TEST_ASSERT_TRUE(payload.isMember("relative_humidity"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 65.43f, payload["relative_humidity"].asFloat());
|
||||
TEST_ASSERT_TRUE(payload.isMember("barometric_pressure"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 1013.27f, payload["barometric_pressure"].asFloat());
|
||||
|
||||
JSONValue *root = JSON::Parse(json.c_str());
|
||||
TEST_ASSERT_NOT_NULL(root);
|
||||
TEST_ASSERT_TRUE(root->IsObject());
|
||||
TEST_ASSERT_TRUE(payload.isMember("gas_resistance"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 50.58f, payload["gas_resistance"].asFloat());
|
||||
TEST_ASSERT_TRUE(payload.isMember("iaq"));
|
||||
TEST_ASSERT_EQUAL(120, payload["iaq"].asInt());
|
||||
|
||||
JSONObject jsonObj = root->AsObject();
|
||||
TEST_ASSERT_TRUE(payload.isMember("voltage"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 3.34f, payload["voltage"].asFloat());
|
||||
TEST_ASSERT_TRUE(payload.isMember("current"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 0.53f, payload["current"].asFloat());
|
||||
|
||||
// Check payload exists
|
||||
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
|
||||
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
|
||||
TEST_ASSERT_TRUE(payload.isMember("lux"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 450.12f, payload["lux"].asFloat());
|
||||
TEST_ASSERT_TRUE(payload.isMember("white_lux"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 380.95f, payload["white_lux"].asFloat());
|
||||
TEST_ASSERT_TRUE(payload.isMember("ir_lux"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 25.37f, payload["ir_lux"].asFloat());
|
||||
TEST_ASSERT_TRUE(payload.isMember("uv_lux"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.68f, payload["uv_lux"].asFloat());
|
||||
|
||||
JSONObject payload = jsonObj["payload"]->AsObject();
|
||||
TEST_ASSERT_TRUE(payload.isMember("distance"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 150.29f, payload["distance"].asFloat());
|
||||
|
||||
// ✅ ALL 22 environment fields MUST be present and correct
|
||||
// If this test fails, it means either:
|
||||
// 1. A new field was added to the protobuf but not to the serializer
|
||||
// 2. The encode_telemetry_environment_metrics_all_fields() function wasn't updated
|
||||
TEST_ASSERT_TRUE(payload.isMember("wind_direction"));
|
||||
TEST_ASSERT_EQUAL(180, payload["wind_direction"].asInt());
|
||||
TEST_ASSERT_TRUE(payload.isMember("wind_speed"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 5.52f, payload["wind_speed"].asFloat());
|
||||
TEST_ASSERT_TRUE(payload.isMember("wind_gust"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 8.24f, payload["wind_gust"].asFloat());
|
||||
TEST_ASSERT_TRUE(payload.isMember("wind_lull"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.13f, payload["wind_lull"].asFloat());
|
||||
|
||||
// Basic environment (3 fields)
|
||||
TEST_ASSERT_TRUE(payload.find("temperature") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 23.56f, payload["temperature"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.find("relative_humidity") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 65.43f, payload["relative_humidity"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.find("barometric_pressure") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 1013.27f, payload["barometric_pressure"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.isMember("weight"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 75.56f, payload["weight"].asFloat());
|
||||
|
||||
// Gas and air quality (2 fields)
|
||||
TEST_ASSERT_TRUE(payload.find("gas_resistance") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 50.58f, payload["gas_resistance"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.find("iaq") != payload.end());
|
||||
TEST_ASSERT_EQUAL(120, (int)payload["iaq"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.isMember("radiation"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 0.13f, payload["radiation"].asFloat());
|
||||
|
||||
// Power measurements (2 fields)
|
||||
TEST_ASSERT_TRUE(payload.find("voltage") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 3.34f, payload["voltage"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.find("current") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 0.53f, payload["current"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.isMember("rainfall_1h"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.57f, payload["rainfall_1h"].asFloat());
|
||||
TEST_ASSERT_TRUE(payload.isMember("rainfall_24h"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.89f, payload["rainfall_24h"].asFloat());
|
||||
|
||||
// Light measurements (4 fields)
|
||||
TEST_ASSERT_TRUE(payload.find("lux") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 450.12f, payload["lux"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.find("white_lux") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 380.95f, payload["white_lux"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.find("ir_lux") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 25.37f, payload["ir_lux"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.find("uv_lux") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.68f, payload["uv_lux"]->AsNumber());
|
||||
|
||||
// Distance measurement (1 field)
|
||||
TEST_ASSERT_TRUE(payload.find("distance") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 150.29f, payload["distance"]->AsNumber());
|
||||
|
||||
// Wind measurements (4 fields)
|
||||
TEST_ASSERT_TRUE(payload.find("wind_direction") != payload.end());
|
||||
TEST_ASSERT_EQUAL(180, (int)payload["wind_direction"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.find("wind_speed") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 5.52f, payload["wind_speed"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.find("wind_gust") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 8.24f, payload["wind_gust"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.find("wind_lull") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.13f, payload["wind_lull"]->AsNumber());
|
||||
|
||||
// Weight measurement (1 field)
|
||||
TEST_ASSERT_TRUE(payload.find("weight") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 75.56f, payload["weight"]->AsNumber());
|
||||
|
||||
// Radiation measurement (1 field)
|
||||
TEST_ASSERT_TRUE(payload.find("radiation") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 0.13f, payload["radiation"]->AsNumber());
|
||||
|
||||
// Rainfall measurements (2 fields)
|
||||
TEST_ASSERT_TRUE(payload.find("rainfall_1h") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.57f, payload["rainfall_1h"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.find("rainfall_24h") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.89f, payload["rainfall_24h"]->AsNumber());
|
||||
|
||||
// Soil measurements (2 fields)
|
||||
TEST_ASSERT_TRUE(payload.find("soil_moisture") != payload.end());
|
||||
TEST_ASSERT_EQUAL(85, (int)payload["soil_moisture"]->AsNumber());
|
||||
TEST_ASSERT_TRUE(payload.find("soil_temperature") != payload.end());
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 18.54f, payload["soil_temperature"]->AsNumber());
|
||||
|
||||
// Total: 22 environment fields
|
||||
// This test ensures 100% coverage of environment metrics
|
||||
|
||||
// Note: JSON float serialization precision may vary due to the underlying library
|
||||
// The important aspect is that all values are numerically accurate within tolerance
|
||||
|
||||
delete root;
|
||||
TEST_ASSERT_TRUE(payload.isMember("soil_moisture"));
|
||||
TEST_ASSERT_EQUAL(85, payload["soil_moisture"].asInt());
|
||||
TEST_ASSERT_TRUE(payload.isMember("soil_temperature"));
|
||||
TEST_ASSERT_FLOAT_WITHIN(0.01f, 18.54f, payload["soil_temperature"].asFloat());
|
||||
}
|
||||
|
||||
// Test that unset environment fields are not present in JSON
|
||||
void test_telemetry_environment_metrics_unset_fields()
|
||||
{
|
||||
uint8_t buffer[256];
|
||||
size_t payload_size = encode_telemetry_environment_metrics_empty(buffer, sizeof(buffer));
|
||||
|
||||
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
|
||||
Json::Value root = serialize_and_get_payload(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size);
|
||||
const Json::Value &payload = root["payload"];
|
||||
|
||||
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
|
||||
TEST_ASSERT_TRUE(json.length() > 0);
|
||||
|
||||
JSONValue *root = JSON::Parse(json.c_str());
|
||||
TEST_ASSERT_NOT_NULL(root);
|
||||
TEST_ASSERT_TRUE(root->IsObject());
|
||||
|
||||
JSONObject jsonObj = root->AsObject();
|
||||
|
||||
// Check payload exists
|
||||
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
|
||||
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
|
||||
|
||||
JSONObject payload = jsonObj["payload"]->AsObject();
|
||||
|
||||
// With completely empty environment metrics, NO fields should be present
|
||||
// Only basic telemetry fields like "time" might be present
|
||||
|
||||
// All 22 environment fields should be absent (none were set)
|
||||
TEST_ASSERT_TRUE(payload.find("temperature") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("relative_humidity") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("barometric_pressure") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("gas_resistance") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("iaq") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("voltage") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("current") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("lux") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("white_lux") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("ir_lux") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("uv_lux") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("distance") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("wind_direction") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("wind_speed") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("wind_gust") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("wind_lull") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("weight") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("radiation") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("rainfall_1h") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("rainfall_24h") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("soil_moisture") == payload.end());
|
||||
TEST_ASSERT_TRUE(payload.find("soil_temperature") == payload.end());
|
||||
|
||||
delete root;
|
||||
TEST_ASSERT_FALSE(payload.isMember("temperature"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("relative_humidity"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("barometric_pressure"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("gas_resistance"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("iaq"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("voltage"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("current"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("lux"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("white_lux"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("ir_lux"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("uv_lux"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("distance"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("wind_direction"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("wind_speed"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("wind_gust"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("wind_lull"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("weight"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("radiation"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("rainfall_1h"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("rainfall_24h"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("soil_moisture"));
|
||||
TEST_ASSERT_FALSE(payload.isMember("soil_temperature"));
|
||||
}
|
||||
|
||||
@@ -1,48 +1,30 @@
|
||||
#include "../test_helpers.h"
|
||||
#include <memory>
|
||||
|
||||
// Helper function to test common packet fields and structure
|
||||
void verify_text_message_packet_structure(const std::string &json, const char *expected_text)
|
||||
static void verify_text_message_packet_structure(const std::string &json, const char *expected_text)
|
||||
{
|
||||
TEST_ASSERT_TRUE(json.length() > 0);
|
||||
|
||||
// Use smart pointer for automatic memory management
|
||||
std::unique_ptr<JSONValue> root(JSON::Parse(json.c_str()));
|
||||
TEST_ASSERT_NOT_NULL(root.get());
|
||||
TEST_ASSERT_TRUE(root->IsObject());
|
||||
Json::Value root = parse_json(json);
|
||||
TEST_ASSERT_TRUE(root.isObject());
|
||||
|
||||
JSONObject jsonObj = root->AsObject();
|
||||
TEST_ASSERT_TRUE(root.isMember("from"));
|
||||
TEST_ASSERT_EQUAL(0x11223344u, root["from"].asUInt());
|
||||
TEST_ASSERT_TRUE(root.isMember("to"));
|
||||
TEST_ASSERT_EQUAL(0x55667788u, root["to"].asUInt());
|
||||
TEST_ASSERT_TRUE(root.isMember("id"));
|
||||
TEST_ASSERT_EQUAL(0x9999u, root["id"].asUInt());
|
||||
|
||||
// Check basic packet fields - use helper function to reduce duplication
|
||||
auto check_field = [&](const char *field, uint32_t expected_value) {
|
||||
auto it = jsonObj.find(field);
|
||||
TEST_ASSERT_TRUE(it != jsonObj.end());
|
||||
TEST_ASSERT_EQUAL(expected_value, (uint32_t)it->second->AsNumber());
|
||||
};
|
||||
TEST_ASSERT_TRUE(root.isMember("type"));
|
||||
TEST_ASSERT_EQUAL_STRING("text", root["type"].asString().c_str());
|
||||
|
||||
check_field("from", 0x11223344);
|
||||
check_field("to", 0x55667788);
|
||||
check_field("id", 0x9999);
|
||||
TEST_ASSERT_TRUE(root.isMember("payload"));
|
||||
TEST_ASSERT_TRUE(root["payload"].isObject());
|
||||
|
||||
// Check message type
|
||||
auto type_it = jsonObj.find("type");
|
||||
TEST_ASSERT_TRUE(type_it != jsonObj.end());
|
||||
TEST_ASSERT_EQUAL_STRING("text", type_it->second->AsString().c_str());
|
||||
|
||||
// Check payload
|
||||
auto payload_it = jsonObj.find("payload");
|
||||
TEST_ASSERT_TRUE(payload_it != jsonObj.end());
|
||||
TEST_ASSERT_TRUE(payload_it->second->IsObject());
|
||||
|
||||
JSONObject payload = payload_it->second->AsObject();
|
||||
auto text_it = payload.find("text");
|
||||
TEST_ASSERT_TRUE(text_it != payload.end());
|
||||
TEST_ASSERT_EQUAL_STRING(expected_text, text_it->second->AsString().c_str());
|
||||
|
||||
// No need for manual delete with smart pointer
|
||||
const Json::Value &payload = root["payload"];
|
||||
TEST_ASSERT_TRUE(payload.isMember("text"));
|
||||
TEST_ASSERT_EQUAL_STRING(expected_text, payload["text"].asString().c_str());
|
||||
}
|
||||
|
||||
// Test TEXT_MESSAGE_APP port
|
||||
void test_text_message_serialization()
|
||||
{
|
||||
const char *test_text = "Hello Meshtastic!";
|
||||
@@ -53,7 +35,6 @@ void test_text_message_serialization()
|
||||
verify_text_message_packet_structure(json, test_text);
|
||||
}
|
||||
|
||||
// Test with nullptr to check robustness
|
||||
void test_text_message_serialization_null()
|
||||
{
|
||||
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, nullptr, 0);
|
||||
@@ -62,11 +43,9 @@ void test_text_message_serialization_null()
|
||||
verify_text_message_packet_structure(json, "");
|
||||
}
|
||||
|
||||
// Test TEXT_MESSAGE_APP port with very long message (boundary testing)
|
||||
void test_text_message_serialization_long_text()
|
||||
{
|
||||
// Test with actual message size limits
|
||||
constexpr size_t MAX_MESSAGE_SIZE = 200; // Typical LoRa payload limit
|
||||
constexpr size_t MAX_MESSAGE_SIZE = 200;
|
||||
std::string long_text(MAX_MESSAGE_SIZE, 'A');
|
||||
|
||||
meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP,
|
||||
@@ -76,30 +55,25 @@ void test_text_message_serialization_long_text()
|
||||
verify_text_message_packet_structure(json, long_text.c_str());
|
||||
}
|
||||
|
||||
// Test with message over size limit (should fail)
|
||||
void test_text_message_serialization_oversized()
|
||||
{
|
||||
constexpr size_t OVERSIZED_MESSAGE = 250; // Over the limit
|
||||
constexpr size_t OVERSIZED_MESSAGE = 250;
|
||||
std::string oversized_text(OVERSIZED_MESSAGE, 'B');
|
||||
|
||||
meshtastic_MeshPacket packet = create_test_packet(
|
||||
meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast<const uint8_t *>(oversized_text.c_str()), oversized_text.length());
|
||||
|
||||
// Should fail or return empty/error
|
||||
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
|
||||
// Should only verify first 234 characters for oversized messages
|
||||
std::string expected_text = oversized_text.substr(0, 234);
|
||||
verify_text_message_packet_structure(json, expected_text.c_str());
|
||||
}
|
||||
|
||||
// Add test for malformed UTF-8 sequences
|
||||
void test_text_message_serialization_invalid_utf8()
|
||||
{
|
||||
const uint8_t invalid_utf8[] = {0xFF, 0xFE, 0xFD, 0x00}; // Invalid UTF-8
|
||||
const uint8_t invalid_utf8[] = {0xFF, 0xFE, 0xFD, 0x00};
|
||||
meshtastic_MeshPacket packet =
|
||||
create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, invalid_utf8, sizeof(invalid_utf8) - 1);
|
||||
|
||||
// Should not crash, may produce replacement characters
|
||||
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
|
||||
TEST_ASSERT_TRUE(json.length() > 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ static size_t encode_waypoint(uint8_t *buffer, size_t buffer_size)
|
||||
waypoint.id = 12345;
|
||||
waypoint.latitude_i = 374208000;
|
||||
waypoint.longitude_i = -1221981000;
|
||||
waypoint.expire = 1609459200 + 3600; // 1 hour from now
|
||||
waypoint.expire = 1609459200 + 3600;
|
||||
strcpy(waypoint.name, "Test Point");
|
||||
strcpy(waypoint.description, "Test waypoint description");
|
||||
|
||||
@@ -15,7 +15,6 @@ static size_t encode_waypoint(uint8_t *buffer, size_t buffer_size)
|
||||
return stream.bytes_written;
|
||||
}
|
||||
|
||||
// Test WAYPOINT_APP port
|
||||
void test_waypoint_serialization()
|
||||
{
|
||||
uint8_t buffer[256];
|
||||
@@ -26,28 +25,20 @@ void test_waypoint_serialization()
|
||||
std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
|
||||
TEST_ASSERT_TRUE(json.length() > 0);
|
||||
|
||||
JSONValue *root = JSON::Parse(json.c_str());
|
||||
TEST_ASSERT_NOT_NULL(root);
|
||||
TEST_ASSERT_TRUE(root->IsObject());
|
||||
Json::Value root = parse_json(json);
|
||||
TEST_ASSERT_TRUE(root.isObject());
|
||||
|
||||
JSONObject jsonObj = root->AsObject();
|
||||
TEST_ASSERT_TRUE(root.isMember("type"));
|
||||
TEST_ASSERT_EQUAL_STRING("waypoint", root["type"].asString().c_str());
|
||||
|
||||
// Check message type
|
||||
TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end());
|
||||
TEST_ASSERT_EQUAL_STRING("waypoint", jsonObj["type"]->AsString().c_str());
|
||||
TEST_ASSERT_TRUE(root.isMember("payload"));
|
||||
TEST_ASSERT_TRUE(root["payload"].isObject());
|
||||
|
||||
// Check payload
|
||||
TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
|
||||
TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
|
||||
const Json::Value &payload = root["payload"];
|
||||
|
||||
JSONObject payload = jsonObj["payload"]->AsObject();
|
||||
TEST_ASSERT_TRUE(payload.isMember("id"));
|
||||
TEST_ASSERT_EQUAL(12345, payload["id"].asInt());
|
||||
|
||||
// Verify waypoint data
|
||||
TEST_ASSERT_TRUE(payload.find("id") != payload.end());
|
||||
TEST_ASSERT_EQUAL(12345, (int)payload["id"]->AsNumber());
|
||||
|
||||
TEST_ASSERT_TRUE(payload.find("name") != payload.end());
|
||||
TEST_ASSERT_EQUAL_STRING("Test Point", payload["name"]->AsString().c_str());
|
||||
|
||||
delete root;
|
||||
TEST_ASSERT_TRUE(payload.isMember("name"));
|
||||
TEST_ASSERT_EQUAL_STRING("Test Point", payload["name"].asString().c_str());
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "serialization/JSON.h"
|
||||
#include "serialization/MeshPacketSerializer.h"
|
||||
#include <Arduino.h>
|
||||
#include <json/json.h>
|
||||
#include <memory>
|
||||
#include <meshtastic/mesh.pb.h>
|
||||
#include <meshtastic/mqtt.pb.h>
|
||||
#include <meshtastic/telemetry.pb.h>
|
||||
@@ -10,6 +11,18 @@
|
||||
#include <pb_encode.h>
|
||||
#include <unity.h>
|
||||
|
||||
// Parse a JSON string into a Json::Value; returns Json::nullValue on failure.
|
||||
static inline Json::Value parse_json(const std::string &s)
|
||||
{
|
||||
Json::CharReaderBuilder b;
|
||||
Json::Value root;
|
||||
std::string errs;
|
||||
std::unique_ptr<Json::CharReader> reader(b.newCharReader());
|
||||
if (!reader->parse(s.c_str(), s.c_str() + s.size(), &root, &errs))
|
||||
return Json::Value();
|
||||
return root;
|
||||
}
|
||||
|
||||
// Helper function to create a test packet with the given port and payload
|
||||
static meshtastic_MeshPacket create_test_packet(meshtastic_PortNum port, const uint8_t *payload, size_t payload_size,
|
||||
int payload_variant = meshtastic_MeshPacket_decoded_tag)
|
||||
@@ -36,7 +49,8 @@ static meshtastic_MeshPacket create_test_packet(meshtastic_PortNum port, const u
|
||||
packet.encrypted.size = payload_size;
|
||||
memcpy(packet.encrypted.bytes, payload, packet.encrypted.size);
|
||||
}
|
||||
memcpy(packet.decoded.payload.bytes, payload, payload_size);
|
||||
if (payload && payload_size)
|
||||
memcpy(packet.decoded.payload.bytes, payload, payload_size);
|
||||
packet.decoded.payload.size = payload_size;
|
||||
packet.decoded.want_response = false;
|
||||
packet.decoded.dest = 0x55667788;
|
||||
|
||||
@@ -13,7 +13,3 @@ board_build.f_cpu = 240000000L
|
||||
upload_protocol = esptool
|
||||
;upload_port = /dev/ttyUSB0
|
||||
upload_speed = 460800
|
||||
lib_deps =
|
||||
${esp32_base.lib_deps}
|
||||
# renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel
|
||||
adafruit/Adafruit NeoPixel@1.15.4
|
||||
|
||||
@@ -8,7 +8,6 @@ build_flags =
|
||||
-I variants/esp32/chatter2
|
||||
-DMESHTASTIC_EXCLUDE_WEBSERVER=1
|
||||
-DMESHTASTIC_EXCLUDE_PAXCOUNTER=1
|
||||
-ULED_BUILTIN
|
||||
|
||||
lib_deps =
|
||||
${esp32_base.lib_deps}
|
||||
|
||||
@@ -10,7 +10,6 @@ build_flags =
|
||||
-D EBYTE_E22
|
||||
-D EBYTE_E22_900M30S ; Assume Tx power curve is identical to 900M30S as there is no documentation
|
||||
-I variants/esp32/diy/9m2ibr_aprs_lora_tracker
|
||||
-ULED_BUILTIN
|
||||
build_src_filter =
|
||||
${esp32_base.build_src_filter}
|
||||
+<../variants/esp32/diy/9m2ibr_aprs_lora_tracker>
|
||||
@@ -14,4 +14,3 @@ build_flags =
|
||||
${esp32_base.build_flags}
|
||||
-D DIY_V1
|
||||
-I variants/esp32/diy/hydra
|
||||
-ULED_BUILTIN
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user